mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-12 15:22:54 +01:00
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
459 lines
12 KiB
JavaScript
459 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const Model = require('./model');
|
|
const { getAbsoluteUrl } = require('./utils');
|
|
|
|
var userFunctionsCache = {};
|
|
|
|
function JavascriptNodeParser(code, options) {
|
|
this.inputs = {};
|
|
this.outputs = {};
|
|
this.error = undefined;
|
|
this.code = code;
|
|
|
|
const node = options ? options.node : undefined;
|
|
this._initializeAPIs();
|
|
|
|
var userCode = userFunctionsCache[code];
|
|
if (!userCode) {
|
|
try {
|
|
userCode = new Function(['define', 'script', 'Node', 'Component'], JavascriptNodeParser.getCodePrefix() + code);
|
|
userFunctionsCache[code] = userCode;
|
|
} catch (e) {
|
|
this.error = e.message;
|
|
console.error(e);
|
|
}
|
|
}
|
|
if (userCode) {
|
|
try {
|
|
userCode(
|
|
this.define.bind(this),
|
|
this.script.bind(this),
|
|
this.apis.Node,
|
|
node ? JavascriptNodeParser.getComponentScopeForNode(node) : {}
|
|
); //noodlJavascriptAPI);
|
|
|
|
this._afterSourced();
|
|
} catch (e) {
|
|
this.error = e.message;
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// First generation API
|
|
JavascriptNodeParser.prototype.define = function (userObject) {
|
|
this.inputs = userObject.inputs || {};
|
|
this.outputs = userObject.outputs || {};
|
|
this.setup = userObject.setup;
|
|
this.change = userObject.run || userObject.change;
|
|
this.destroy = userObject.destroy;
|
|
|
|
this.definedObject = userObject;
|
|
};
|
|
|
|
// Second generation API
|
|
function _scriptExtend(_this) {
|
|
var _extended = {
|
|
inputs: _this.inputs || {},
|
|
outputs: _this.outputs || {},
|
|
|
|
setup: function (inputs, outputs) {
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
|
|
this.setOutputs = function (states) {
|
|
for (var key in states) {
|
|
this.outputs[key] = states[key];
|
|
this.flagOutputDirty(key);
|
|
}
|
|
};
|
|
|
|
if (_this.methods) {
|
|
for (var key in _this.methods) {
|
|
this[key] = _this.methods[key];
|
|
}
|
|
}
|
|
|
|
_this.setup && _this.setup.apply(this);
|
|
},
|
|
|
|
destroy: function (inputs, outputs) {
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
|
|
_this.destroy && _this.destroy.apply(this);
|
|
},
|
|
|
|
change: function (inputs, outputs) {
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
|
|
// Detect property changed
|
|
var old = this._oldInputs || {};
|
|
|
|
if (_this.changed) {
|
|
for (var key in inputs) {
|
|
if (inputs[key] !== old[key]) {
|
|
var changedFunction = _this.changed[key];
|
|
if (typeof changedFunction === 'function') changedFunction.apply(this, [inputs[key], old[key]]);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._oldInputs = Object.assign({}, inputs);
|
|
}
|
|
};
|
|
|
|
if (_this.signals) {
|
|
for (var key in _this.signals) {
|
|
_extended[key] = _this.signals[key];
|
|
|
|
_extended.inputs[key] = 'signal';
|
|
}
|
|
}
|
|
|
|
return _extended;
|
|
}
|
|
|
|
JavascriptNodeParser.prototype.script = function (userObject) {
|
|
var _userObjectExtended = _scriptExtend(userObject);
|
|
this.inputs = _userObjectExtended.inputs || {};
|
|
this.outputs = _userObjectExtended.outputs || {};
|
|
this.setup = _userObjectExtended.setup;
|
|
this.change = _userObjectExtended.run || _userObjectExtended.change;
|
|
this.destroy = _userObjectExtended.destroy;
|
|
|
|
this.definedObject = _userObjectExtended;
|
|
};
|
|
|
|
// Third generation API
|
|
|
|
// Node.Setters.Hej = function(value) {...}
|
|
// Node.OnInputsChanged = function() {...}
|
|
// Node.Signals.Hej = function(value) {...}
|
|
// Node.OnInit = function() {...}
|
|
// Node.OnDestroy = function() {...}
|
|
// Node.Inputs.A
|
|
// Node.Outputs.A = 10;
|
|
// Node.Outputs.Done()
|
|
// Node.setOutputs({...})
|
|
// Component.Object, Component.ParentObject
|
|
JavascriptNodeParser.prototype._initializeAPIs = function () {
|
|
this.apis = {};
|
|
|
|
this.apis.Node = {
|
|
Inputs: {},
|
|
Outputs: {},
|
|
Signals: {},
|
|
Setters: {}
|
|
};
|
|
};
|
|
|
|
JavascriptNodeParser.prototype._afterSourced = function () {
|
|
if (this.definedObject !== undefined) return; // a legacy API have been used
|
|
|
|
var _Node = this.apis.Node;
|
|
|
|
// Merge inputs and outputs from node extension
|
|
this.inputs = Object.assign({}, _Node.Inputs || {});
|
|
this.outputs = Object.assign({}, _Node.Outputs || {});
|
|
|
|
this.setup = function (inputs, outputs) {
|
|
const _this = this;
|
|
|
|
_Node.setOutputs = function (_outputs) {
|
|
for (var key in _outputs) {
|
|
outputs[key] = _outputs[key];
|
|
_this.flagOutputDirty(key);
|
|
}
|
|
};
|
|
|
|
_Node.OnInit && _Node.OnInit.apply(this);
|
|
};
|
|
|
|
this.destroy = _Node.OnDestroy || this.destory;
|
|
this.change = (inputs, outputs, changedInputs) => {
|
|
for (var key in changedInputs) {
|
|
if (typeof _Node.Setters[key] === 'function') {
|
|
_Node.Setters[key](inputs[key]);
|
|
}
|
|
}
|
|
|
|
if (typeof _Node.OnInputsChanged === 'function') {
|
|
_Node.OnInputsChanged();
|
|
}
|
|
};
|
|
|
|
this.definedObject = {
|
|
inputs: this.inputs,
|
|
outputs: this.outputs,
|
|
setup: this.setup,
|
|
destroy: this.destroy,
|
|
change: this.change
|
|
};
|
|
|
|
// Set all signals as signal inputs
|
|
if (_Node.Signals !== undefined) {
|
|
for (var key in _Node.Signals) {
|
|
if (typeof _Node.Signals[key] === 'function') {
|
|
this.inputs[key] = 'signal';
|
|
|
|
this.definedObject[key] = _Node.Signals[key];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
JavascriptNodeParser.createFromCode = function (code, options) {
|
|
return new JavascriptNodeParser(code, options);
|
|
};
|
|
|
|
JavascriptNodeParser.createFromURL = function (url, callback, options) {
|
|
url = getAbsoluteUrl(url);
|
|
|
|
var xhr = new window.XMLHttpRequest();
|
|
xhr.open('GET', url, true);
|
|
xhr.onreadystatechange = function () {
|
|
// XMLHttpRequest.DONE = 4, but torped runtime doesn't support enum
|
|
if (this.readyState === 4 || this.readyState === XMLHttpRequest.DONE) {
|
|
callback(new JavascriptNodeParser(this.response));
|
|
}
|
|
};
|
|
xhr.onerror = function () {
|
|
console.log('Failed to request', url);
|
|
};
|
|
xhr.send();
|
|
};
|
|
|
|
JavascriptNodeParser.parseAndAddPortsFromScript = function (script, ports, options) {
|
|
// Extract inputs and outputs
|
|
function _exists(port) {
|
|
if (typeof port === 'string') return ports.find((p) => p.name === port) !== undefined;
|
|
else return ports.find((p) => p.name === port.name && p.plug === port.plug) !== undefined;
|
|
}
|
|
|
|
function _addPortsFromMatch(match, options) {
|
|
if (match === undefined || match === null) return;
|
|
|
|
const unique = {};
|
|
for (const _s of match) {
|
|
let name = _s[1];
|
|
if (name === undefined) continue;
|
|
|
|
unique[name] = true;
|
|
}
|
|
|
|
Object.keys(unique).forEach((p) => {
|
|
if (
|
|
_exists({
|
|
name: options.prefix + p,
|
|
plug: options.plug
|
|
})
|
|
)
|
|
return;
|
|
|
|
ports.push({
|
|
name: options.prefix + p,
|
|
displayName: p,
|
|
plug: options.plug,
|
|
type: options.type,
|
|
group: options.group
|
|
});
|
|
});
|
|
}
|
|
|
|
// const scriptWithoutComments = script.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,''); // Remove comments
|
|
|
|
// Regular Inputs. notation
|
|
if (!options.skipInputs) {
|
|
_addPortsFromMatch(script.matchAll(/Inputs\.([A-Za-z0-9_]+)/g), {
|
|
type: options.inputType || '*',
|
|
plug: 'input',
|
|
group: options.inputGroup || 'Inputs',
|
|
prefix: options.inputPrefix || ''
|
|
});
|
|
|
|
// Inputs with Inputs["A"] notation
|
|
_addPortsFromMatch(script.matchAll(/Inputs\s*\[\s*(?:'|")(.*)(?:'|")\s*\]/g), {
|
|
type: options.inputType || '*',
|
|
plug: 'inputs',
|
|
group: options.inputGroup || 'Inputs',
|
|
prefix: options.inputPrefix || ''
|
|
});
|
|
}
|
|
|
|
if (!options.skipOutputs) {
|
|
if (!options.skipOutputSignals) {
|
|
// Output signals, Output.Done()
|
|
_addPortsFromMatch(script.matchAll(/Outputs\.([A-Za-z0-9]+)\s*\(\s*\)/g), {
|
|
type: 'signal',
|
|
plug: 'output',
|
|
group: 'Outputs',
|
|
prefix: options.outputPrefix || ''
|
|
});
|
|
|
|
// Output signals, Outputs["Done"]()
|
|
_addPortsFromMatch(script.matchAll(/Outputs\s*\[\s*(?:'|")(.*)(?:'|")\s*\]\(\s*\)/g), {
|
|
type: 'signal',
|
|
plug: 'output',
|
|
group: 'Outputs',
|
|
prefix: options.outputPrefix || ''
|
|
});
|
|
}
|
|
|
|
if (!options.skipRegularOutputs) {
|
|
// Regular output Outputs. notation
|
|
_addPortsFromMatch(script.matchAll(/Outputs\.([A-Za-z0-9_]+)/g), {
|
|
type: '*',
|
|
plug: 'output',
|
|
group: 'Outputs',
|
|
prefix: options.outputPrefix || ''
|
|
});
|
|
|
|
// Outputs with Outputs["A"] notation
|
|
_addPortsFromMatch(script.matchAll(/Outputs\s*\[\s*\"([^\"]*)\"\s*\]/g), {
|
|
type: '*',
|
|
plug: 'output',
|
|
group: 'Outputs',
|
|
prefix: options.outputPrefix || ''
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
JavascriptNodeParser.prototype.getPorts = function () {
|
|
var ports = [];
|
|
|
|
var self = this;
|
|
|
|
Object.keys(this.inputs).forEach(function (name) {
|
|
var inputPort = self.inputs[name];
|
|
|
|
var port = {
|
|
name: name,
|
|
plug: 'input'
|
|
};
|
|
if (typeof inputPort === 'string') {
|
|
port.type = {
|
|
name: inputPort
|
|
};
|
|
port.group = 'Inputs';
|
|
} else {
|
|
for (var p in inputPort) {
|
|
port[p] = inputPort[p];
|
|
}
|
|
}
|
|
|
|
ports.push(port);
|
|
});
|
|
|
|
Object.keys(this.outputs).forEach(function (name) {
|
|
ports.push({
|
|
name: name,
|
|
type: {
|
|
name: self.outputs[name]
|
|
},
|
|
plug: 'output',
|
|
group: 'Outputs'
|
|
});
|
|
});
|
|
|
|
JavascriptNodeParser.parseAndAddPortsFromScript(this.code, ports, {});
|
|
|
|
return ports;
|
|
};
|
|
|
|
const _componentScopes = {};
|
|
|
|
function _findParentComponentStateModelId(node) {
|
|
function getParentComponent(component) {
|
|
let parent;
|
|
if (component.getRoots().length > 0) {
|
|
//visual
|
|
const root = component.getRoots()[0];
|
|
|
|
if (root.getVisualParentNode) {
|
|
//regular visual node
|
|
if (root.getVisualParentNode()) {
|
|
parent = root.getVisualParentNode().nodeScope.componentOwner;
|
|
}
|
|
} else if (root.parentNodeScope) {
|
|
//component instance node
|
|
parent = component.parentNodeScope.componentOwner;
|
|
}
|
|
} else if (component.parentNodeScope) {
|
|
parent = component.parentNodeScope.componentOwner;
|
|
}
|
|
|
|
//check that a parent exists and that the component is different
|
|
if (parent && parent.nodeScope && parent.nodeScope.componentOwner !== component) {
|
|
//check if parent has a Component State node
|
|
if (parent.nodeScope.getNodesWithType('net.noodl.ComponentObject').length > 0) {
|
|
return parent;
|
|
}
|
|
|
|
if (parent.nodeScope.getNodesWithType('Component State').length > 0) {
|
|
return parent;
|
|
}
|
|
|
|
//if not, continue searching up the tree
|
|
return getParentComponent(parent);
|
|
}
|
|
}
|
|
|
|
const parent = getParentComponent(node.nodeScope.componentOwner);
|
|
if (!parent) return;
|
|
|
|
//this._internal.parentComponentName = parent.name;
|
|
|
|
return 'componentState' + parent.getInstanceId();
|
|
}
|
|
|
|
function _findForEachModel(node) {
|
|
var component = node.nodeScope.componentOwner;
|
|
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
|
component = component.parentNodeScope.componentOwner;
|
|
}
|
|
return component !== undefined ? component._forEachModel : undefined;
|
|
}
|
|
|
|
JavascriptNodeParser.getComponentScopeForNode = function (node) {
|
|
const componentId = node.nodeScope.componentOwner.getInstanceId();
|
|
|
|
if (_componentScopes[componentId] === undefined) {
|
|
_componentScopes[componentId] = {};
|
|
|
|
const componentObject = (node.nodeScope.modelScope || Model).get('componentState' + componentId);
|
|
|
|
_componentScopes[componentId].Object = componentObject;
|
|
}
|
|
|
|
// This should be done each time, since the component can be moved
|
|
const parentComponentObjectId = _findParentComponentStateModelId(node);
|
|
const parentComponentObject =
|
|
parentComponentObjectId !== undefined
|
|
? (node.nodeScope.modelScope || Model).get(parentComponentObjectId)
|
|
: undefined;
|
|
|
|
_componentScopes[componentId].ParentObject = parentComponentObject;
|
|
|
|
// Set the for each model
|
|
_componentScopes[componentId].RepeaterObject = _findForEachModel(node);
|
|
|
|
return _componentScopes[componentId];
|
|
};
|
|
|
|
JavascriptNodeParser.getCodePrefix = function () {
|
|
// API
|
|
return "const Script = (typeof Node !== 'undefined')?Node:undefined;\n";
|
|
};
|
|
|
|
JavascriptNodeParser.createNoodlAPI = function () {
|
|
// If we are running in browser mode and there is a global Noodl API object, use it. If not
|
|
// create a new one for this scope.
|
|
return typeof window !== 'undefined' && window.Noodl !== undefined ? window.Noodl : {};
|
|
};
|
|
|
|
module.exports = JavascriptNodeParser;
|