UNPKG

@scion-scxml/scxml

Version:

An implementation of SCXML in JavaScript.

838 lines (704 loc) 30.5 kB
/** * Accept a scjson document as input, either from a file or via stdin. * Generate a JavaScript module as output. * This module should be customizable: * plain object literal if appropriate * simple self-invoking function (for use in scion-scxml) * UMD in probably all other cases. although we could make it CommonJS/AMD/etc. */ var SourceNode = require('./sourcemap').SourceNode; var constants = require('../constants'); var to_js_identifier = require("text-to-js-identifier"); var util = require('../util'); var sax = require("@scion-scxml/sax"); const tagRegistry = require('../runtime/tagRegistry').registry; const DEFAULT_INVOKE_TYPE = 'http://www.w3.org/TR/scxml/'; //TODO: optimization: if the scjson does not contain a datamodel or any actions, then just dump out the object literal as the module //TODO: we should also encode the document name. accept as command-line argument, or embed it in the scjson itself, maybe? var printTrace = false; function generateFnName(actionType,action){ return '$' + actionType + '_l' + action.$line + '_c' + action.$column; } var FN_ARGS = '(_event)'; var stripNsPrefixRe = /^(?:{([^}]*)})?(.*)$/; function stripAttrNsPrefix(attrName){ var m = attrName.match(stripNsPrefixRe); return m[2]; } function parseJsCode(fnBody, action, docUrl, node){ return node.add(fnBody); } function generateFnDeclaration(fnName,fnBody,actionOrInvoke,docUrl,isExpression,parseGeneratedCodeForSourceMap){ if(printTrace) console.log('generateFnDeclaration',fnName,fnBody); var node = new module.exports.SourceNode(); node.add('function ' + fnName + FN_ARGS + '{\n'); if(isExpression){ var returnNode = new module.exports.SourceNode(actionOrInvoke.$line + 1, actionOrInvoke.$column, docUrl, 'return '); node.add(returnNode); } if(parseGeneratedCodeForSourceMap){ let action = actionOrInvoke; //FYI, he is an action module.exports.parseJsCode(fnBody, action, docUrl, node, isExpression); }else{ node.add( new module.exports.SourceNode( actionOrInvoke.$line + 1, actionOrInvoke.$column, docUrl, fnBody.trim() ) ); } if(isExpression){ //node.add(';'); } node.add( new module.exports.SourceNode( actionOrInvoke.$closeLine + 1, actionOrInvoke.$closeColumn, docUrl, '\n};\n' ) ); node.add( fnName + '.tagname=\'' + actionOrInvoke.$type + '\';\n' + fnName + '.line=' + actionOrInvoke.$line + ';\n' + fnName + '.column=' + actionOrInvoke.$column + ';\n' ); return node; } ModuleBuilder.prototype.generateFnCall = function(fnName, ...args){ if(printTrace) console.log('generateFnCall',fnName); return `${fnName}.${args.length ? 'call' : 'apply'}(this, ${args.length ? args.join(',') : 'arguments' })`; } // invokeid: // node.invokeid function getConstructorFunctionName(node){ // constructor function name: // if(node.$type is 'invoke'){ // return invokeid(node) // else if(node.$type is 'scxml'){ // if(node.name){ // return node.name; // } else { // return 'root'; // } // }else{ // throw new Error('Unexpected node type'); // } let x; if(node.$type === 'invoke'){ x = generateFnName('invoke',node); }else if(node.$type === 'scxml'){ if(node.name){ x = node.name; } else { x = 'root'; } } else{ throw new Error('Expected node type invoke or root state.'); } return `${x}Constructor`; } ModuleBuilder.prototype.getInvokeId = function(invoke, parentState, processAttr){ var invokeId; if(invoke.id){ invokeId = processAttr('id', parentState); } else if(invoke.idlocation){ invokeId = processAttr('idlocation', parentState); } else{ //generate a new id do{ invokeId = `${parentState.id}.invokeid_${this.invokeIdCounter++}`; //make sure we dont clobber an existing invokeid } while(this.invokeIdAccumulator.indexOf(invokeId) > -1) invokeId = JSON.stringify(invokeId); } return invokeId; }; ModuleBuilder.prototype.generateInvokeFunction = function(invoke, parentState) { if(printTrace) console.log('generateActionFunction',invoke); const processAttr = this.processSendOrInvokeAttr.bind(this, invoke); var fnName = generateFnName('invoke',invoke); let invokeId = this.getInvokeId(invoke, parentState, processAttr); if(invoke.finalize && invoke.finalize.actions) this.replaceActions(invoke.finalize,'actions'); const autoforward = invoke.autoforward === 'true'; var fnBody = ` ${invoke.idlocation ? `${fnName}.id = ${invokeId};` : ''} this.invoke({ "autoforward" : ${autoforward}, "type" : ${processAttr('type', parentState) || JSON.stringify(DEFAULT_INVOKE_TYPE)}, "src" : ${processAttr('src', parentState)}, "id" : ${fnName}.id, ${ invoke.content && invoke.content.rootState ? `"constructorFunction" : ${getConstructorFunctionName(invoke)},` : '' } ${ invoke.content && invoke.content.expr ? `"content" : ${this.generateFnCall(this.generateAttributeExpression(invoke.content, 'expr', invoke.$type))},` : '' } ${ (invoke.namelist || (invoke.params && invoke.params.length)) ? `"params" : ${this.generateFnCall(actionWithNamelistAndParamsToProps(invoke, this))},` : '' } "docUrl" : ${JSON.stringify(this.docUrl)} }); `; var fnDec = generateFnDeclaration(fnName,fnBody,invoke,this.docUrl,false,false); fnDec += ` ${fnName}.autoforward = ${autoforward}; `; if(!invoke.idlocation) { fnDec += ` ${fnName}.id = ${invokeId}; `; } if(invoke.finalize && invoke.finalize.actions){ fnDec += ` ${fnName}.finalize = ${JSON.stringify(invoke.finalize.actions).replace(REFERENCE_MARKER_RE,'$1')}; `; } this.fnDecAccumulator.push(fnDec); return fnName; } ModuleBuilder.prototype.generateActionFunction = function(action) { if(printTrace) console.log('generateActionFunction',action); const m = action.$type.match(stripNsPrefixRe); const ns = m[1] || constants.SCXMLNS; const tagName = m[2]; if(!tagRegistry[ns] || (tagRegistry[ns] && !tagRegistry[ns][tagName])) throw new Error(`Unable to find action tag generator function for namespace ${ns} and tag name ${tagName}`); var fnName = generateFnName(action.$type,action); var fnBody = tagRegistry[ns][tagName](action, this); var fnDec = generateFnDeclaration(fnName,fnBody,action,this.docUrl,false,action.$type === 'script'); this.fnDecAccumulator.push(fnDec); return fnName; } function normalizeWhitespace (str) { return str.replace(/^\s+|\s+$|\s+(?=\s)/g, '').replace(/\s/g, " "); } function parseContentAsString(content){ return JSON.stringify(normalizeWhitespace(content)) } function parseContentAsXml(content){ let toReturn; try { //then try to detect if he is an XML document //FIXME: we probably want to defer normalizing whitespace until this point so that the XML doc can preserve whitespace, while the below string has whitespace normalized var parser = sax.parser(true, {trim : false, xmlns : true}); parser.onerror = function () { // an error happened. toReturn = parseContentAsString(content); }; parser.onend = function () { //do the thing toReturn = `this.parseXmlStringAsDOM(${JSON.stringify(content)})`; }; parser.write(content).close(); } catch (e) { } return toReturn; } ModuleBuilder.prototype.generateExpressionFunction = function(expressionType,exprObj,isContent,containerType){ if(printTrace) console.log('generateExpressionFunction',expressionType,exprObj); var fnName = generateFnName(expressionType,exprObj); var fnBody; if(isContent){ //slightly complicated rules for interpreting the values of content tags, and contents of <data> and <assign> if(containerType === 'invoke'){ //interpret as string only. the invoker only wants a string. fnBody = parseContentAsString(exprObj.content); }else{ //for <data>, <assign>, <send> and donedata //try to parse as JSON, then, xml, then fall back to string try { //first try to detect if he is a JSON object JSON.parse(exprObj.content); fnBody = exprObj.content; } catch (e) { fnBody = parseContentAsXml(exprObj.content); } } } else { fnBody = exprObj.expr } var fnDec = generateFnDeclaration(fnName,fnBody,exprObj,this.docUrl,true,true); this.fnDecAccumulator.push(fnDec); return fnName; } ModuleBuilder.prototype.generateAttributeExpression = function(attrContainer, attrName, containerType){ if(printTrace) console.log('generateAttributeExpression',attrContainer,attrName); return this.generateExpressionFunction(stripAttrNsPrefix(attrName), attrContainer[attrName], attrName === 'content', containerType); } var REFERENCE_MARKER = '__UNQUOTE__', REFERENCE_MARKER_RE = new RegExp('"' + REFERENCE_MARKER + '(.*)' + REFERENCE_MARKER + '"','g') ; //TODO: need to split this into two parts: one that declares the variables in the datamodel at the top of the module scope, //and another single function that inits the model needs to contain a reference to this init function, //and the interpreter must know about it. should be optional. //call it $scion_init_datamodel. function generateDatamodelDeclaration(datamodelAccumulator){ if (!datamodelAccumulator.length) { return undefined; } return 'var ' + datamodelAccumulator. map(function(datamodel){ return datamodel.declarations.map(function(data){ return data.id; }); }).reduce(function(a,b){return a.concat(b);},[]).join(", ") + ";"; } var SERIALIZE_DATAMODEL_FN_NAME = '$serializeDatamodel'; function generateDatamodelSerializerFn(datamodelAccumulator){ return 'function ' + SERIALIZE_DATAMODEL_FN_NAME + '(){\n' + ' return {\n' + datamodelAccumulator.map(function(datamodel){ return datamodel.declarations.map(function(data){ return ' "' + data.id + '" : ' + data.id; }) }).reduce(function(a,b){return a.concat(b);},[]).join(',\n') + '\n' + ' };\n' + '}\n'; } var DESERIALIZE_DATAMODEL_FN_NAME = '$deserializeDatamodel', DESERIALIZE_DATAMODEL_FN_ARG = '$serializedDatamodel'; function generateDatamodelDeserializerFn(datamodelAccumulator){ return 'function ' + DESERIALIZE_DATAMODEL_FN_NAME + '(' + DESERIALIZE_DATAMODEL_FN_ARG + '){\n' + datamodelAccumulator.map(function(datamodel){ return datamodel.declarations.map(function(data){ return ' ' + data.id + ' = ' + DESERIALIZE_DATAMODEL_FN_ARG + '["' + data.id + '"];'; }) }).reduce(function(a,b){return a.concat(b);},[]).join('\n') + '\n' + '}\n'; } function generateDatamodelInitFn(datamodel, builder){ //this guard guarantees it will only fire once //invoke all datamodel expresions var fnName = generateFnName('datamodel',datamodel); var delcarationsToInit = datamodel.declarations. filter(function(data){return data.expr || data.content;}); if(delcarationsToInit.length){ var fnBody = delcarationsToInit.map(function(data){ return 'if(typeof ' + data.id + ' === "undefined") ' + data.id + ' = ' + builder.generateFnCall(data.expr ? this.generateExpressionFunction('data', data.expr, false, 'data') : this.generateExpressionFunction('data', data.content, true, 'data') ) + ';\n'; }, builder).join(''); var fnDec = generateFnDeclaration(fnName,fnBody,datamodel,builder.docUrl,false,false); builder.fnDecAccumulator.push(fnDec); return fnName; } } function generateSmObjectLiteral(rootState){ //pretty simple return JSON.stringify(rootState, undefined, 1).replace(REFERENCE_MARKER_RE,'$1'); } function dumpHeader(strict){ var d = new Date(); return '//Generated on ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString() + ' by the SCION SCXML compiler\n' + (strict ? "'use strict';\n" : ""); } function generateFactoryFunctionWrapper(o, constructorFunctionName, rootState){ var root = new module.exports.SourceNode(null,null,null,[ //o.headerString + '\n' , 'function ' + constructorFunctionName + '(_x,_sessionid,_ioprocessors,In){\n' , ' var _name = \'' + rootState.name + '\';\n', new module.exports.SourceNode(null,null,null,""), //create a dummy node at index 2, which we might use to inject datamodel and scripts o.sendString, o.sendIdLocationString, o.invokeIdLocationString, o.datamodelDeserializerFnDeclaration, o.datamodelSerializerFnDeclaration, o.actionFunctionDeclarations, 'return ' + o.objectLiteralString + ';\n', '}\n' ] ); return root; } ModuleBuilder.prototype.getConstructorFunctionName = function(){ return getConstructorFunctionName(this.invokeConstructor || this.rootState) }; ModuleBuilder.prototype.generateModule = function(){ var rootState = this.rootState; var options = this.options; if(this.datamodelAccumulator.length && !this.isLateBinding){ let fnNames = this.datamodelAccumulator.map(function(datamodel){ return generateDatamodelInitFn(datamodel, this); }, this).filter(function(name){return name}).map(markAsReference); //generalize him as an entry action on the root state rootState.onEntry = rootState.onEntry || []; //make sure that datamodel initialization fn comes before all other entry actions rootState.onEntry = fnNames.concat(rootState.onEntry); } //attach datamodel serialization functions rootState[DESERIALIZE_DATAMODEL_FN_NAME] = markAsReference(DESERIALIZE_DATAMODEL_FN_NAME); rootState[SERIALIZE_DATAMODEL_FN_NAME] = markAsReference(SERIALIZE_DATAMODEL_FN_NAME); //console.log('rootState.rootScripts',rootState.rootScripts); //TODO: support other module formats (AMD, UMD, module pattern) var o = { headerString : dumpHeader(options.strict), sendString : (this.documentHasSendAction ? getDelayInMsFnStr + '\n' : ''), sendIdLocationString : (this.documentHasSendActionWithIdlocationAttribute ? generateIdlocationGenerator('send', GENERATE_SENDID_FN_NAME, this.sendIdAccumulator) : ''), invokeIdLocationString : (this.documentHasInvokeActionWithIdlocationAttribute ? generateIdlocationGenerator('invoke', GENERATE_INVOKEID_FN_NAME, this.invokeIdAccumulator) : ''), datamodelDeserializerFnDeclaration : generateDatamodelDeserializerFn(this.datamodelAccumulator), datamodelSerializerFnDeclaration : generateDatamodelSerializerFn(this.datamodelAccumulator), actionFunctionDeclarations : this.fnDecAccumulator }; delete rootState.rootScripts; //this doesn't need to be in there this.rootState.docUrl = this.docUrl; o.objectLiteralString = generateSmObjectLiteral(rootState); var s = generateFactoryFunctionWrapper(o, this.getConstructorFunctionName(), this.rootState, options); return s; } function markAsReference(fnName){ return REFERENCE_MARKER + fnName + REFERENCE_MARKER; } ModuleBuilder.prototype.replaceActionCode = function(actionContainer, actionPropertyName, functionGenerator){ functionGenerator = functionGenerator || this.generateActionFunction; //default arg if(actionContainer[actionPropertyName]){ var actions = Array.isArray(actionContainer[actionPropertyName]) ? actionContainer[actionPropertyName] : [actionContainer[actionPropertyName]] ; actionContainer[actionPropertyName] = actions.map(function(actionOrArray){ if(Array.isArray(actionOrArray)){ return actionOrArray.map(function(action){ return functionGenerator.call(this, action, actionContainer); }, this).map(markAsReference); }else{ return markAsReference(functionGenerator.call(this, actionOrArray, actionContainer)); } },this); if(actionContainer[actionPropertyName].length === 1){ actionContainer[actionPropertyName] = actionContainer[actionPropertyName][0]; } } } ModuleBuilder.prototype.replaceActions = ModuleBuilder.prototype.replaceActionCode; ModuleBuilder.prototype.replaceInvokes = function(invokesContainer){ this.replaceActionCode(invokesContainer,'invokes', this.generateInvokeFunction); } ModuleBuilder.prototype.visitState = function(){ var genValue = this.stateGen.next(); if (genValue.done) { this._finish(); return; } var state = genValue.value; if(state.invokes) this.replaceInvokes(state); if(state.onExit) this.replaceActions(state,'onExit'); if(state.onEntry) this.replaceActions(state,'onEntry'); //accumulate datamodels if (state.datamodel) { this.datamodelAccumulator.push(state.datamodel); if(this.isLateBinding){ let fnName = generateDatamodelInitFn(state.datamodel, this); if(fnName){ state.onEntry = state.onEntry || []; state.onEntry = [markAsReference(fnName)].concat(state.onEntry); } } } if(state.$type === 'final' && state.donedata){ state.donedata = markAsReference(this.constructSendEventData(state.donedata, 'donedata')); } if(state.transitions) { for (var i = 0, len = state.transitions.length; i < len; i++) { var transition = state.transitions[i]; this.replaceActions(transition,'onTransition'); if(transition.cond){ transition.cond = markAsReference(this.generateAttributeExpression(transition,'cond')); } } } //clean up as we go delete state.datamodel; setImmediate(function(self) { self.visitState(); }, this); } /** * The uncooked SCION module * @param {string} [name] The name of the module derived from the scxml root element's name attribute * @param {string} datamodel The raw datamodel declarations * @param {Array<ScriptNode>} rootScripts A collection of 0 or more script nodes * Each node contains a src property which references an external js resource * or a content property which references a string containing the uncooked js * @param {string} scxmlModule A massive string containing the generated scxml program * less the parts enumerated above */ function SCJsonRawModule(rootState, invokeConstructors, docUrl) { this.rootState = rootState; this.docUrl = docUrl; this.invokeConstructors = invokeConstructors; } function InvokeConstructor(rootScripts, datamodel, moduleSourceNode, docUrl){ this.module = moduleSourceNode; this.datamodel = datamodel; this.rootScripts = rootScripts; this.docUrl = docUrl; } InvokeConstructor.prototype.prepareModuleRootNode = function(hostContext){ var scriptPromises = util.fetchScripts(this, hostContext); return Promise.all(scriptPromises).then(function resolved() { var rootNode = new module.exports.SourceNode(null, null, null); var injectionNode = this.module.children[2]; if(this.datamodel) { injectionNode.add(this.datamodel); injectionNode.add('\n'); } this.rootScripts.forEach(function(rootScript){ if(rootScript.$wrap){ injectionNode.add(rootScript.$wrap(rootScript.content)); }else{ if(rootScript.src){ module.exports.parseJsCode(rootScript.content, {$line : 0, $column : 0}, rootScript.src, injectionNode, false); } else { module.exports.parseJsCode(rootScript.content, {$line : rootScript.$line, $column : rootScript.$column}, this.docUrl, injectionNode, false); } } injectionNode.add('\n'); },this) rootNode.add(this.module); return rootNode; }.bind(this)); } function* genStates(state) { yield state; if (state.states) { for (var j = 0, len = state.states.length; j < len; j++) { yield* genStates(state.states[j]); } } } function ModuleBuilder(docUrl, rootState, invokeIdAccumulator, invokeConstructor, options) { this.docUrl = docUrl; this.rootState = rootState; if (!rootState.rootScripts) { rootState.rootScripts = []; } this.isLateBinding = rootState.binding === 'late'; this.externalActionScripts = new Set(); this.options = options; this.datamodelAccumulator = []; this.fnDecAccumulator = []; this.sendIdAccumulator = []; this.invokeConstructor = invokeConstructor; this.invokeIdAccumulator = invokeIdAccumulator || []; this.invokeIdCounter = 0; this.documentHasSendAction = false; this.documentHasSendActionWithIdlocationAttribute = false; this.documentHasInvokeActionWithIdlocationAttribute = false; this.resolve = undefined; this.reject = undefined; this.stateGen = genStates(this.rootState); } ModuleBuilder.prototype.build = function() { var self = this; return new Promise(function(resolve, reject) { self.resolve = resolve; self.reject = reject; self.visitState(); }); } ModuleBuilder.prototype._finish = function() { // grab the root scripts before generateDatamodelDeclaration hackily deletes them var rootScripts = this.rootState.rootScripts; var dataModel = generateDatamodelDeclaration(this.datamodelAccumulator); var scxmlModule = this.generateModule(); this.resolve(new InvokeConstructor( rootScripts, dataModel, scxmlModule, this.docUrl )); } function startTraversal(docUrl, rootState, options){ if (!options) { options = {}; } let invokeConstructorAccumulator = []; let rootStatesToProcess = [[null, rootState]]; do { let invokeConstructor, rs; [invokeConstructor, rs] = rootStatesToProcess.shift(); let invokeWithStaticContentAccumulator, invokeIdAccumulator; [ invokeWithStaticContentAccumulator, invokeIdAccumulator ] = analyze(rs); //find all the invokes with contents var moduleBuilder = new ModuleBuilder(docUrl, rs, invokeIdAccumulator, invokeConstructor, options); invokeConstructorAccumulator.push(moduleBuilder.build()); rootStatesToProcess.push.apply(rootStatesToProcess, invokeWithStaticContentAccumulator.map(invoke => [invoke, invoke.content.rootState])); } while(rootStatesToProcess.length) return Promise.all(invokeConstructorAccumulator).then(function(invokeConstructors){ return new SCJsonRawModule( rootState, invokeConstructors, docUrl ); }); } function isCommonjs(hostContext){ return hostContext.moduleFormat === 'node' || hostContext.moduleFormat === 'commonjs'; } function analyze(rootState){ let invokeWithStaticContentAccumulator = [], invokeIdAccumulator = []; function visitState(state){ if(state.invokes){ (Array.isArray(state.invokes) ? state.invokes : [state.invokes]).forEach((invoke) => { if(invoke.content && invoke.content.rootState){ invokeWithStaticContentAccumulator.push(invoke); } if(invoke.id){ // add id to idAccumulator invokeIdAccumulator.push(invoke.id); } }); } if(state.states) state.states.forEach(visitState); } visitState(rootState); return [ invokeWithStaticContentAccumulator, invokeIdAccumulator ]; } ModuleBuilder.prototype.safelyAddVariableToDatamodelAccumulator = function(variableName,lineNum,colNum){ if(!this.datamodelAccumulator.some(function(datamodel){ return datamodel.declarations.some(function(data) { return data.id === variableName; })})){ // add datamodel declaration to the accumulator this.datamodelAccumulator.push({ $type : 'datamodel', declarations : [{ $line : lineNum, $col : colNum, id : variableName }] }); } } /** * Handles an externally referenced script within an executable content block * @param {object} action The script action * @return {string} A call to the named function that will be injected at model preparation time * @see document-string-to-model#prepare */ ModuleBuilder.prototype.handleExternalActionScript = function(action) { // base the generated function name on the fully-qualified url, NOT on its position in the file var fnName = to_js_identifier(action.src); // Only load the script once. It will be evaluated as many times as it is referenced. if (!this.externalActionScripts.has(action.src)) { this.externalActionScripts.add(action.src); action.$wrap = function(body) { return generateFnDeclaration(fnName,body,{$line : 0, $column:0},action.src,false,true); }.bind(this); this.rootState.rootScripts.push(action); } return this.generateFnCall(fnName); } ModuleBuilder.prototype.constructSendEventData = function(action, actionType){ if(action.expr || action.content){ return this.generateAttributeExpression(action, action.expr ? 'expr' : 'content', actionType || action.$type); } else { return actionWithNamelistAndParamsToProps(action, this); } } ModuleBuilder.prototype.processSendOrInvokeAttr = function(container, attr, parentState){ if(!(container.$type === 'send' || container.$type === 'invoke')) throw new Error('processSendOrInvokeAttr requires container of type send or invoke'); if(attr === 'id'){ let arr = this[container.$type === 'invoke' ? 'invokeIdAccumulator' : 'sendIdAccumulator']; let val = container[attr]; if( arr.indexOf(val) === -1 ){ arr.push(val); } } var exprName = attr + 'expr'; var fnName; if(attr === 'idlocation'){ this[container.$type === 'invoke' ? 'documentHasInvokeActionWithIdlocationAttribute' : 'documentHasSendActionWithIdlocationAttribute'] = true; //FIXME: overwriting this variable is a bit ugly. //if we're going to generate this expr on the fly, it would be better to clone the container. container[attr].expr = container[attr].expr + '=' + (container.$type === 'invoke' ? this.generateFnCall(GENERATE_INVOKEID_FN_NAME, JSON.stringify(parentState.id)) : this.generateFnCall(GENERATE_SENDID_FN_NAME)); //TODO: get parent state id for invoke fnName = this.generateAttributeExpression(container, attr, container.$type); return this.generateFnCall(fnName); }else if(container[exprName]){ fnName = this.generateAttributeExpression(container, exprName, container.$type); return this.generateFnCall(fnName); }else if(container[attr]){ return JSON.stringify(container[attr]); }else{ return null; } } function actionWithNamelistAndParamsToProps(action, builder){ var props = []; //namelist if(action.namelist){ action.namelist.expr.trim().split(/ +/).forEach(function(name){ props.push('"' + name + '"' + ":" + name); //FIXME: should add some kind of stack trace here. this is hard, though, because it aggregates multiple expressions to a single line/column }); } //params if(action.params && action.params.length){ action.params.forEach(function(param){ if(param.expr){ props.push('"' + param.name + '"' + ":" + builder.generateFnCall(builder.generateAttributeExpression(param, 'expr'))); }else if(param.location){ props.push('"' + param.name + '"' + ":" + builder.generateFnCall(builder.generateAttributeExpression(param, 'location'))); } }); } var fnBody = "{\n" + props.join(',\n') + "}\n"; var fnName = generateFnName('senddata',action); var fnDec = generateFnDeclaration(fnName,fnBody,action,builder.docUrl,true,false); builder.fnDecAccumulator.push(fnDec); return fnName; } const getDelayInMsFnStr = `function getDelayInMs(delayString){ if(typeof delayString === 'string') { if (delayString.slice(-2) === "ms") { return parseFloat(delayString.slice(0, -2)); } else if (delayString.slice(-1) === "s") { return parseFloat(delayString.slice(0, -1)) * 1000; } else if (delayString.slice(-1) === "m") { return parseFloat(delayString.slice(0, -1)) * 1000 * 60; } else { return parseFloat(delayString); } }else if (typeof delayString === 'number'){ return delayString; }else{ return 0; } }`; //flow through the code and //generate idlocationGenerator if we find var GENERATE_SENDID_FN_NAME = '$generateSendId'; var GENERATE_INVOKEID_FN_NAME = '$generateInvokeId'; function generateIdlocationGenerator(type, fnName, idAccumulator){ return ` var $${type}IdCounter = 0; var $${type}IdAccumulator = ${ JSON.stringify(idAccumulator) } ; function ${fnName}(${type === 'invoke' ? 'parentStateId' : ''}){ var id; do{ id = ${type === 'invoke' ? 'parentStateId' : '"$scion"'} + ".${type}id_" + $${type}IdCounter++; //make sure we dont clobber an existing sendid or invokeid } while($${type}IdAccumulator.indexOf(id) > -1) return id; }; ` } module.exports = { SCJsonRawModule : SCJsonRawModule, getConstructorFunctionName : getConstructorFunctionName, startTraversal : startTraversal, parseJsCode : parseJsCode, dumpHeader : dumpHeader, isCommonjs : isCommonjs, SourceNode : SourceNode };