carbone
Version:
Fast, Simple and Powerful report generator. Injects JSON and produces PDF, DOCX, XLSX, ODT, PPTX, ODS, ...!
855 lines (802 loc) • 37.7 kB
JavaScript
var extracter = require('./extracter');
var parser = require('./parser');
var helper = require('./helper');
var builder = {
/**
* Build the xml according to the data
* @param {String} xml xml to parse
* @param {Object|Array} data
* @param {Object} options {
* 'complement' : data which accessible with the market {c.}
* 'formatters' : formatters
* 'lang' : selected lang, ex. "fr"
* 'translations' : all translations {fr: {}, en: {}, es: {}}
* 'existingVariables' : existing variables (Array returned by parser.findVariables())
* 'extension' : template file extension
* }
* @param {Function} callback
*/
buildXML : function (xml, data, options, callback) {
if (typeof(options) === 'function') {
callback = options;
options = {};
}
// find declared variable {#myVar }
parser.findVariables(xml, options.existingVariables, function (err, xmlWithoutVariable, variables) {
// find markers { d. } or { c. }
parser.findMarkers(xmlWithoutVariable, function (err, xmlWithoutMarkers, markers) {
// exit if there is no markers
if (markers.length === 0) {
return callback(null, xmlWithoutVariable);
}
parser.preprocessMarkers(markers, variables, function (err, preprocessedMarkers) {
var _xmlParts = [];
var _xmlResult = '';
var _builder;
if (options.isDebugActive === true) {
options.debugInfo.markers = [...options.debugInfo.markers, ...preprocessedMarkers.map((marker) => '{'+marker.name.replace(/^_root\./, '')+'}' )];
}
try {
var _dynamicDescriptor = extracter.splitMarkers(preprocessedMarkers);
var _descriptor = extracter.splitXml(xmlWithoutMarkers, _dynamicDescriptor);
_descriptor = extracter.buildSortedHierarchy(_descriptor);
_descriptor = extracter.deleteAndMoveNestedParts(_descriptor);
var _builder = builder.getBuilderFunction(_descriptor, options.formatters);
var _obj = {
d : data,
c : options.complement
};
options.stopPropagation = false;
_xmlParts = _builder(_obj, options, helper, _builder.builderDictionary);
_xmlResult = builder.assembleXmlParts(_xmlParts, 20, _builder.builderDictionary); // TODO -> adapt the depth of the sort according to the maximum depth in _xmlParts
}
catch (e) {
return callback(e, null);
}
return callback(null, _xmlResult);
});
});
});
},
/**
* Creates a string of the formatter(s) function(s) call of the attribute
* INTERNAL USE ONLY
*
* @param {Function} getSafeValue generate safe value accessor
* @param {String} attr. Example 'd.number'
* @param {Array} formatters. Example [ 'int', 'toFixed(2)' ]
* @param {Object} existingFormatters
* @param {Boolean} onlyFormatterWhichInjectXML : if undefined|false, it returns only formatters which do not inject XML
* if true, it returns only formatters which do inject XML. These formatters have
* the property canInjectXML = true
* @return {String}. Example 'toFixed(int(d.number), 2)'
*/
getFormatterString : function (getSafeValue, varName, contextName, formatters, existingFormatters, onlyFormatterWhichInjectXML) {
var _lineOfCodes = [];
function getInjectedVariable (variable) {
var _injectedArgument = getSafeValue(variable);
if (variable.startsWith('.') === true) {
var _nbPoint = 0;
for (; variable[_nbPoint] === '.'; _nbPoint++) {
// count number of point nothing
}
var _dynamicVariable = variable.slice(_nbPoint);
_injectedArgument = 'helper.getValueOfPath( '+contextName+'.parentsData['+(_nbPoint-1)+'], '+getSafeValue(_dynamicVariable)+')';
}
return _injectedArgument;
}
for (var i = 0; i < formatters.length; i++) {
var _formatter = formatters[i];
// Percent-Encode the commas when the paramater is string with single quotes: ',' to '%2c'
_formatter = _formatter.replace(/[']{1}[^']*[']{1}/g, function (matched) {
return matched.replace(/,/g, '%2c');
});
var _indexFirstParenthesis = _formatter.indexOf('(');
var _argumentStr = '';
var _functionStr = '';
// this function contains arguments
if (_indexFirstParenthesis !== -1) {
_functionStr = _formatter.slice(0, _indexFirstParenthesis);
var _indexLastParenthesis = _formatter.lastIndexOf(')'); // TODO error if last Parenthesis is not found
var _arguments = _formatter.slice(_indexFirstParenthesis+1, _indexLastParenthesis).split(/ *, */);
for (var j = 0; j < _arguments.length; j++) {
if (_arguments[j] !== '') {
// remove existing quotes (everything is converted to a string for the momemt. It should change in the future)
// Percent-Decode the commas to obtain the initial string argument: '%2c' to ','
var _argument = _arguments[j].replace(/^ *'?/, '').replace(/'? *$/, '').replace(/%2c/g, ',');
if (existingFormatters?.[_functionStr]?.isAcceptingMathExpression === true) {
_argumentStr += ', ' + parser.parseMathematicalExpression(_argument, getInjectedVariable)
}
else {
_argumentStr += ', ' + getInjectedVariable(_argument);
}
}
}
}
else {
_functionStr = _formatter;
}
if (existingFormatters[_functionStr] === undefined) {
var _alternativeFnName = helper.findClosest(_functionStr, existingFormatters);
throw Error('Formatter "'+_functionStr+'" does not exist. Do you mean "'+_alternativeFnName+'"?');
}
if ( (existingFormatters[_functionStr].canInjectXML === true && onlyFormatterWhichInjectXML === true)
|| (existingFormatters[_functionStr].canInjectXML !== true && onlyFormatterWhichInjectXML !== true)) {
_lineOfCodes.push(varName +' = formatters.' + _functionStr + '.call(' + contextName + ', ' + varName + _argumentStr + ');\n');
}
}
var _str = _lineOfCodes.join('if('+contextName+'.stopPropagation === false){\n');
var _nbClosingBraces = _lineOfCodes.length - 1;
while (_nbClosingBraces-- > 0) {
_str += '}';
}
return _str;
},
/**
* build the condition string which will be used to filter rows
*
* @param {Function} getSafeVar : generate safe variable
* @param {Function} getSafeValue : generate safe value accessor
* @param {array} conditions : array of conditions. Example [{'left':'sort', 'operator':'>', 'right':'10'}]
* @param {string} codeIfTrue : code to insert if the condition is true. Example 'row = true'
* @param {string} prefix (optional) : if we need to create local variable, this prefix will be added in front of the variable.
* @param {boolean} inverseCondition (optional, default false) : if true, it will inverse the condition
* @param {string} forceObjectTested (optional) : use forceObjectTested instead of _condition.left.parent
* @return {string}. Example : 'if(d.number['sort'] > 10) { row = true }'
*/
getFilterString : function (getSafeVar, getSafeValue, conditions, codeIfTrue, prefix, inverseCondition, forceObjectTested) {
prefix = prefix || '';
if (!codeIfTrue || !(conditions instanceof Array) || conditions.length ===0) {
return '';
}
var _str = '';
var _alreadyDeclaredFilter = {};
var _declareStr = '';
_str += 'if(';
_str += (inverseCondition === true) ? '!(':'(';
for (var c = 0; c < conditions.length; c++) {
var _condition = conditions[c];
var _attr = _condition.left.attr;
var _objName = _condition.left.parent;
if (forceObjectTested) {
_objName = forceObjectTested;
}
// if the left part is an object
var _objectSeparatorIndex = _attr.indexOf('.');
if (_objectSeparatorIndex !== -1) {
var _obj = _attr.slice(0, _objectSeparatorIndex);
_attr = _attr.slice(_objectSeparatorIndex+1);
var _subObjName = '_'+prefix+_obj+'_filter';
if (_alreadyDeclaredFilter[_subObjName] === undefined) {
_declareStr += 'var '+getSafeVar(_subObjName)+'='+getSafeVar(_objName)+'['+getSafeValue(_obj)+'];\n';
}
_objName = _subObjName;
}
if (_attr === 'i') {
var _rightOperator = parseInt(_condition.right, 10);
var _arrayName = _condition.left.parent;
if (parseInt(_condition.right, 10) < 0) {
_rightOperator = getSafeVar(_arrayName) + '_array_length ' + parseInt(_condition.right, 10);
}
_str += getSafeVar(_arrayName) + '_i '+_condition.operator + _rightOperator;
}
else {
if (_alreadyDeclaredFilter[_objName] === undefined) {
_str += getSafeVar(_objName) + ' && ';
}
_str += getSafeVar(_objName)+'['+getSafeValue(_attr)+']';
if (_condition.right === 'true' || _condition.right === 'false') {
// for boolean, convert data to string to accept both cases ('true' == true) and (true == true) for backward compatibility with Carbone v1 & v2
_str += "+''";
}
_str += _condition.operator + getSafeValue(helper.removeQuote(_condition.right));
}
if (c < conditions.length - 1) {
_str += ' && ';
}
_alreadyDeclaredFilter[_objName] = true;
}
_str += ')){\n ';
_str += codeIfTrue +';\n';
_str += ' }';
return _declareStr + _str;
},
/**
* Assemble all xml parts and return the final result, the xml string with data
*
* @param {array} arrayOfStringPart : special array which is generated by the function returned by getBuilderFunction
* @param {integer} sortDepth : description
* @param {array} builderDictionary : xml part coming from template
* @return {string} final result
*/
assembleXmlParts : function (arrayOfStringPart, sortDepth, builderDictionary) {
var _res = '';
builder.sortXmlParts(arrayOfStringPart);
var _rowInfo = [];
var _prevDepth = 0;
var _prevPart = {pos : []};
var _arrayLevel = 0;
var _loopIds = {};
var _hideBlock = 0;
var _prevHideBlock = 0;
// TODO MANAGE sortDepth
sortDepth = (sortDepth) ? sortDepth : 1;
// init _rowInfo
for (var loop = 0; loop <= sortDepth; loop++) {
_rowInfo.push({
xmlPos : 0,
rowShow : 0
});
}
for (var i = 0; i < arrayOfStringPart.length; i++) {
var _part = arrayOfStringPart[i];
var _str = '';
// Get count value if needed
builder.getLoopIteration(_loopIds, _part);
// keep parts if positions are not the same as the last one OR if the it the beginning of an array
if ((_prevPart.rowStart === true && _part.rowStart !== true) || isArrayEqual(_prevPart.pos, _part.pos) === false) {
_prevPart = _part;
}
else {
continue;
}
_prevHideBlock = _hideBlock;
if (_part.hide !== undefined) {
_hideBlock += (_hideBlock && 1) | _part.hide;
if (_hideBlock < 0) {
_hideBlock = 0;
}
}
if (_hideBlock > 0 && _prevHideBlock > 0) {
continue;
}
// include "before" XML string all the time, except at the end of if block
if (!(_hideBlock === 0 && _prevHideBlock > 0)) {
_str = _part.bef !== undefined ? builderDictionary[_part.bef] : '';
}
// include "after" XML string and the value to inject (_part.str) all the time, except at the beginning of if block
if (!(_hideBlock > 0 && _prevHideBlock === 0)) {
_str += _part.str;
_str += _part.aft !== undefined ? builderDictionary[_part.aft] : '';
}
_res += _str;
var _depth = _part.pos.length;
if (_prevDepth < _depth) {
_arrayLevel++;
}
else if (_prevDepth > _depth) {
_arrayLevel--;
}
_prevDepth = _depth;
// TODO. Protection if sortDepth is too short
if (_rowInfo[_arrayLevel] === undefined) {
_rowInfo[_arrayLevel] = {
xmlPos : 0,
rowShow : 0
};
}
_rowInfo[_arrayLevel].rowShow |= _part.rowShow;
if (_part.rowStart===true) {
_rowInfo[_arrayLevel].xmlPos = _res.length - _str.length;
}
else if (_part.rowEnd===true) {
if (_rowInfo[_arrayLevel].rowShow === 0) {
_res = _res.slice(0, _rowInfo[_arrayLevel].xmlPos);
}
if (_arrayLevel > 0) {
_rowInfo[_arrayLevel-1].rowShow |= _rowInfo[_arrayLevel].rowShow;
}
_rowInfo[_arrayLevel].rowShow = 0;
}
}
return _res;
},
/**
* Replace __COUNT_*_*__ by the right value (part.str)
* INTERNAL USE ONLY (USED ONLY BY assembleXmlParts)
*
* @param {Object} loopIds Stored loop IDs with their start value
* @param {Object} part Xml part
*/
getLoopIteration : function (loopIds, part) {
var _rowColumnRegex = /__COUNT_(\d*?)_(\d*?)__/;
var _rowColumnMatch = _rowColumnRegex.exec(part.str);
// If we match a COUNT marker
if (_rowColumnMatch !== null && part.rowShow === true) {
var _loopId = _rowColumnMatch[1];
var _loopStart = _rowColumnMatch[2];
// Define loop ID if does not exist
if (loopIds[_loopId] === undefined) {
loopIds[_loopId] = _loopStart;
}
// And replace by the good value
part.str = part.str.replace(_rowColumnMatch[0], loopIds[_loopId]++);
}
},
/**
* Sort the special array which is generated by the function returned by getBuilderFunction.
* INTERNAL USE ONLY (USED ONLY BY assembleXmlParts)
*
* @param {array} arrayToSort : array of objects
*/
sortXmlParts : function (arrayToSort) {
function compare (a, b) {
var i = 0;
var _a = a.pos[i];
var _b = b.pos[i];
var _aLength = a.pos.length;
var _bLength = b.pos.length;
while (_a === _b && i < _aLength && i < _bLength) {
i++;
_a = a.pos[i];
_b = b.pos[i];
}
if (_a !== undefined && _b !== undefined) {
return (_a > _b) ? 1 : -1;
}
if (_a === undefined && _b === undefined && a.rowShow !== b.rowShow) {
return (a.rowShow === false && b.rowShow === true) ? 1 : -1;
}
return _aLength - _bLength;
}
return arrayToSort.sort(compare);
},
/**
* Call a function for each array we are leaving.
* Update the array "currentlyVisitedArrays": it removes array names we leave
*
* @param {array} currentlyVisitedArrays (modified) : currently visited arrays. Ex ['d', 'site']
* @param {object} objDependencyDescriptor : descriptor returned by extracter
* @param {string} nextAttrName : next attribute name of the descriptor we will visit
* @param {function} execute(exitedArrayName) : called for each array we are leaving.
*/
forEachArrayExit : function (currentlyVisitedArrays, objDependencyDescriptor, nextAttrName, execute) {
var _currentParentAttr = '';
if (objDependencyDescriptor[nextAttrName] !== undefined) {
_currentParentAttr = objDependencyDescriptor[nextAttrName].parent;
}
if (currentlyVisitedArrays.length>0) {
var _firstParentOfTypeArray = _currentParentAttr;
// go up in the tree until we meet an array
while (_firstParentOfTypeArray !== '' && objDependencyDescriptor[_firstParentOfTypeArray].type !== 'array') {
_firstParentOfTypeArray = objDependencyDescriptor[_firstParentOfTypeArray].parent;
}
var _lastArrayName = currentlyVisitedArrays[currentlyVisitedArrays.length-1];
var _nextArrayXmlDepth = objDependencyDescriptor[nextAttrName].depth;
if (_nextArrayXmlDepth === undefined) {
_nextArrayXmlDepth = -1;
if (objDependencyDescriptor[_firstParentOfTypeArray] !== undefined && objDependencyDescriptor[_firstParentOfTypeArray].depth !== undefined) {
_nextArrayXmlDepth = objDependencyDescriptor[_firstParentOfTypeArray].depth;
}
}
var _lastArrayXmlDepth = objDependencyDescriptor[_lastArrayName].depth || Number.MAX_SAFE_INTEGER;
while (_firstParentOfTypeArray !== _lastArrayName && currentlyVisitedArrays.length>0 && _nextArrayXmlDepth <= _lastArrayXmlDepth) {
execute(_lastArrayName);
currentlyVisitedArrays.pop();
if (currentlyVisitedArrays.length>0) {
_lastArrayName = currentlyVisitedArrays[currentlyVisitedArrays.length-1];
_lastArrayXmlDepth = objDependencyDescriptor[_lastArrayName].depth || Number.MAX_SAFE_INTEGER;
}
}
}
},
/**
* Generate safe and unique variable names generator for builder
*
* @return {Function} a function which generate variable names
*/
generateSafeJSVariable : function () {
let _dictionary = new Map();
return (jsVariable) => {
let _alreadyCreatedVar = _dictionary.get(jsVariable);
if (_alreadyCreatedVar !== undefined) {
return _alreadyCreatedVar;
}
let _newVar = '_gV' + _dictionary.size;
_dictionary.set(jsVariable, _newVar);
return _newVar;
};
},
/**
* Generate a functions which store and get values coming from insecure
* source (XMl, carbone markers in document, formatters variable, ...) in a dictionary
*
* @param {String} dictionaryName name of the dictionary in the builder function
*/
generateSafeJSValueAccessor : function (dictionaryName) {
let _dictionaryIndex = new Map();
let _dictionary = [];
function getIndex (xmlOrConstantValue) {
let _dictIndex = _dictionaryIndex.get(xmlOrConstantValue);
if (_dictIndex !== undefined) {
return _dictIndex;
}
_dictIndex = (_dictionary.push(xmlOrConstantValue) - 1);
_dictionaryIndex.set(xmlOrConstantValue, _dictIndex);
return _dictIndex;
}
return {
// get index where the value or XML part is stored in the dictionary
getIndex : getIndex,
// get the JS code to access securely to a dictionary's value in the builder function
get : function (xmlOrConstantValue) {
let _dictIndex = getIndex(xmlOrConstantValue);
return dictionaryName + '[' + _dictIndex + ']';
},
// get the dictionary
getDictionary : function () {
return _dictionary;
}
};
},
/**
* Generate a function which will return an array of strings.
* This array of strings will be sorted and assemble by assembleXmlParts to get final result.
*
* @param {object} descriptor : data descriptor computed by the analyzer
* @return {function} f
*/
getBuilderFunction : function (descriptor, existingFormatters) {
var that = this;
// declare an object which will contain all the code (by section) of the generated function
var _code = {
init : '',
prev : '',
main : '',
add : function () {
var _lastIndex = arguments.length - 1;
var _str = arguments[_lastIndex];
for (var i = 0; i < _lastIndex; i++) {
this[arguments[i]] += _str;
}
}
};
var _getSafeVar = that.generateSafeJSVariable();
var _dictionaryName = '_dictionary';
var _safeAccessor = that.generateSafeJSValueAccessor(_dictionaryName);
var _getSafeValue = _safeAccessor.get;
var _getXMLStrIndex = _safeAccessor.getIndex;
_code.add('prev', addIfNotExist.toString()+'\n'+removeFrom.toString()+'\n');
_code.add('init', "var _strResult = '';\n");
_code.add('init', 'var '+_getSafeVar('_root') + '= (data !== null)?data:{};\n');
_code.add('init', 'var _strPart = {};\n');
_code.add('init', 'var _strParts = [];\n');
_code.add('init', 'var _xmlPos = [0];\n');
_code.add('init', 'var formatters = context.formatters;\n'); // used by getFormatterString
var _atLeastOneSpecialIterator = false;
// _str += "var _nestedArrayParams = [];\n";
var _lastArrayEnd = 0;
var _nestedArray = [];
var _keepHighestPosition = 0;
var _nbCustomIterator = 0;
var _dynamicData = descriptor.dynamicData || {};
var _staticData = descriptor.staticData || {};
var _hierarchy = descriptor.hierarchy || [];
if (_staticData.before !== '') {
_code.add('main', '_strPart = {\n');
_code.add('main', " 'pos' : [0],\n");
_code.add('main', " 'str' : '',\n");
_code.add('main', " 'bef' : "+_getXMLStrIndex(_staticData.before) + '\n');
_code.add('main', '};\n');
_code.add('main', '_strParts.push(_strPart);\n');
}
// no iterator template optimisation
var _noIteratorTemplate = true;
for (var exitedArrayName in _dynamicData ) {
if (_dynamicData[exitedArrayName].iterators !== undefined && _dynamicData[exitedArrayName].iterators.length > 0 ) {
_noIteratorTemplate = false;
// TODO break;
}
}
if ( _noIteratorTemplate ) {
_code.add('main', 'var _registeredXml = {};\n');
}
// declare all variables
for (var _objNameDeclaration in _dynamicData) {
// the root "_" object is already declared above
if (_objNameDeclaration !== '_root' ) {
_code.add('init', 'var '+_getSafeVar(_objNameDeclaration)+' = {};\n');
}
}
// For each object, generate the code
for (var _objIndex = 0; _objIndex < _hierarchy.length; _objIndex++) {
var _objName = _hierarchy[_objIndex];
var _realObjName = _getSafeValue(_dynamicData[_objName].name);
var _type = _dynamicData[_objName].type;
var _objParentName = _dynamicData[_objName].parent;
var _objParentNames = _dynamicData[_objName].parents.reverse();
var _xmlParts = _dynamicData[_objName].xmlParts;
var _arrayDepth = _dynamicData[_objName].depth || 0;
var _beforeStr = _dynamicData[_objName].before;
// close nested for-loop brackets
that.forEachArrayExit(_nestedArray, _dynamicData, _objName, function (exitedArrayName) {
_code.add('prev','main', '}\n');
if (_dynamicData[exitedArrayName].iterators !== undefined) {
_nbCustomIterator -= _dynamicData[exitedArrayName].iterators.length-1;
}
});
// Object type
if (_type === 'object') {
// declare any nested object
if (_objName!=='_root') {
_code.add('prev','main', _getSafeVar(_objName)+'='+'('+_getSafeVar(_objParentName)+' instanceof Object)?'+_getSafeVar(_objParentName)+'['+_realObjName+']:{};\n');
}
}
else if (_type === 'objectInArray') {
var _arrayOfObjectNameG = _getSafeVar(_objName)+'_array';
var _arrayOfObjectIndexNameG = _getSafeVar(_objName)+'_i';
_code.add('prev','main', 'var ' + _arrayOfObjectNameG+'='+'('+_getSafeVar(_objParentName)+' instanceof Object)? '+_getSafeVar(_objParentName)+'['+_realObjName+']:[];\n');
var _conditionToFindObject = _dynamicData[_objName].conditions || [];
var _objNameTemp = _objName+'_temp';
_code.add('prev', 'main', _getSafeVar(_objName) + '={};\n');
_code.add('prev', 'main', 'var '+_arrayOfObjectNameG+'_length = ('+_arrayOfObjectNameG+' instanceof Array)?'+_arrayOfObjectNameG+'.length : 0;\n');
_code.add('prev', 'main', 'for (var '+_arrayOfObjectIndexNameG+' = 0; '+_arrayOfObjectIndexNameG+' < '+_arrayOfObjectNameG+'_length ; '+_arrayOfObjectIndexNameG+'++) {\n');
_code.add('prev', 'main', ' var '+_getSafeVar(_objNameTemp)+' = '+_arrayOfObjectNameG+'['+_arrayOfObjectIndexNameG+'];\n');
_code.add('prev', 'main', that.getFilterString(_getSafeVar, _getSafeValue, _conditionToFindObject, _getSafeVar(_objName)+'='+_arrayOfObjectNameG+'['+_arrayOfObjectIndexNameG+'];\n break;\n', _objNameTemp, false, _objNameTemp));
_code.add('prev', 'main', '}\n');
}
// Array type
else if (_type === 'array') {
var _posStart = _dynamicData[_objName].position.start;
var _posEnd = _dynamicData[_objName].position.end;
var _arrayIndexNameG = _getSafeVar(_objName)+'_i';
var _arrayNameG = _getSafeVar(_objName)+'_array';
var _iterators = _dynamicData[_objName].iterators;
var _repeater = _dynamicData[_objName].repeater;
var _repeaterGlobalCounter = _getSafeVar(_objName)+'_repeatCount';
var _containSpecialIterator = _dynamicData[_objName].containSpecialIterator;
_code.add('main', '_xmlPos['+(_nbCustomIterator+_arrayDepth*2-2)+'] = '+_posStart+';\n');
_code.add('main', '_xmlPos['+(_nbCustomIterator+_arrayDepth*2-1)+'] = '+_posStart+';\n');
// declare any nested object
if (_objName!=='_root') {
_code.add('prev', 'main', 'var '+_arrayNameG+'='+'('+ _getSafeVar(_objParentName)+' instanceof Object) ?'+ _getSafeVar(_objParentName)+'['+_realObjName+']:[];\n');
}
else {
_code.add('prev', 'main', 'var '+_arrayNameG+'='+ _getSafeVar(_objName)+';\n');
}
// keep the end of the array
_lastArrayEnd = (_posEnd>_lastArrayEnd)? _posEnd : _lastArrayEnd;
// add xml which are before the array repetition section
if (_beforeStr !== undefined && _beforeStr !== '') {
_code.add('main', '_strPart = {\n');
_code.add('main', " 'pos' : _xmlPos.slice(0, "+(_nbCustomIterator+_arrayDepth*2-1)+'),\n');
_code.add('main', " 'str' : '',\n");
_code.add('main', " 'bef' : "+_getXMLStrIndex(_beforeStr) + '\n');
_code.add('main', '};\n');
_code.add('main', '_strParts.push(_strPart);\n');
}
// store array name
_nestedArray.push(_objName);
var _missingIteratorArrayNameG = _arrayNameG+'_missingIterators';
if (_containSpecialIterator===true) {
_missingIteratorArrayNameG = _arrayNameG+'_missingIterators';
_code.add('main' , 'var '+_missingIteratorArrayNameG+'=[];\n');
}
_code.add('prev','main', 'var '+_arrayNameG+'_length = 0;\n');
_code.add('prev','main', 'if ('+_arrayNameG+' instanceof Array) {\n');
_code.add('prev','main', _arrayNameG+'_length = '+_arrayNameG+'.length;\n');
_code.add('prev','main', '} else if ('+_arrayNameG+' instanceof Object) {\n');
_code.add('prev','main', _arrayNameG+' = Object.entries('+_arrayNameG+').map((a) => { return {att : a[0], val : a[1] } });\n');
_code.add('prev','main', _arrayNameG+'_length = '+_arrayNameG+'.length;\n');
_code.add('prev','main', '}\n');
if ( _repeater ) {
_code.add('prev','main', 'var '+_repeaterGlobalCounter+' = 0;\n');
_nestedArray.push(_objName);
}
_code.add('prev','main', 'for (var '+_arrayIndexNameG+' = 0; '+_arrayIndexNameG+' < '+_arrayNameG+'_length ');
if (_containSpecialIterator===true) {
_code.add( 'main', ' || '+_missingIteratorArrayNameG+'.length>0');
}
_code.add('prev','main', ' ; '+_arrayIndexNameG+'++) {\n');
_code.add('prev','main', ' var '+ _getSafeVar(_objName)+' = '+_arrayNameG+'['+_arrayIndexNameG+'];\n');
// use a custom iterator?
for (var it = 0; it < _iterators.length; it++) {
var _iterator = _iterators[it];
var _iteratorObjG = _getSafeValue(_iterator.obj);
var _iteratorAttrG = _getSafeValue(_iterator.attr);
if (it>0) {
_nbCustomIterator++;
}
if (_iterator.attr === 'i') {
_code.add('main', ' _xmlPos['+(_nbCustomIterator+_arrayDepth*2-1)+'] = '+_arrayIndexNameG+';\n');
if ( _repeater ) {
var _repeaterAttr = _getSafeValue(_repeater);
var _repeaterName = _getSafeVar(_objName)+'_repeat';
_code.add('prev', 'main', ' let '+_repeaterName+' = '+_getSafeVar(_objName)+'['+_repeaterAttr+'];\n');
_code.add('prev', 'main', ' if ( '+_repeaterName+' > 200 ) { throw Error("The repeater cannot be above 200"); }\n');
_code.add('main', 'while( '+_repeaterName+'-- > 0) {\n');
_code.add('main', ' _xmlPos['+(_nbCustomIterator+_arrayDepth*2-1)+'] = '+_repeaterGlobalCounter+'++;\n');
}
}
else {
var _iteratorNameG = _getSafeVar(_objName)+'_it';
var _iteratorObjNameG = _getSafeVar(_objName)+'_itObj';
_code.add('prev', 'main', ' var '+_iteratorNameG+' = 0;\n'); // TODO return errors if the iterator is not defined
if (_containSpecialIterator===true) {
_code.add('main', 'if('+ _getSafeVar(_objName) +' !== undefined){\n');
}
// the iterator is inside an object
if (_iterator.obj !== undefined) {
_code.add('prev','main', ' var '+_iteratorObjNameG+' = '+_getSafeVar(_objName)+'['+_iteratorObjG+'];\n');
_code.add('prev','main', ' if('+_iteratorObjNameG+' !== undefined) {\n');
_code.add('prev','main', ' '+_iteratorNameG+' = '+_iteratorObjNameG+'['+_iteratorAttrG+'];\n');
_code.add('prev','main', ' }\n');
}
else {
_code.add('prev','main', ' '+_iteratorNameG+' = '+_getSafeVar(_objName)+'['+_iteratorAttrG+'];\n');
}
if (_containSpecialIterator===true) {
_code.add('main', ' removeFrom('+_missingIteratorArrayNameG+', '+_iteratorNameG+');\n');
_code.add('main', '}else{\n');
_code.add('main', ' '+_iteratorNameG+' = '+_missingIteratorArrayNameG+'.pop();\n');
_code.add('main', '}\n');
}
_code.add('main', ' _xmlPos['+(_nbCustomIterator+_arrayDepth*2-1)+'] = '+_iteratorNameG+';\n');
// count the number of custom iterator
// _str += " _xmlPos["+(_nbCustomIterator+_arrayDepth*2-1)+"] = "+_arrayIndexNameG+";\n";
if (_iterator.isSpecial === true) {
_atLeastOneSpecialIterator = true;
var _allIteratorArrayNameG = _iteratorNameG + '_wanted';
_code.main = _code.main.replace(_missingIteratorArrayNameG+'=[]', _missingIteratorArrayNameG+'='+_allIteratorArrayNameG+'.slice()');
_code.add('init', 'var '+_allIteratorArrayNameG+'=[];\n');
_code.add('prev', ' addIfNotExist('+_allIteratorArrayNameG+', '+_iteratorNameG+');\n');
}
}
}
}
// Generate code which will concatenate each xml parts
for (var i = 0; i < _xmlParts.length; i++) {
var _xmlPart = _xmlParts[i];
var _dataObj = _xmlPart.obj;
var _dataAttr = _xmlPart.attr;
var _partDepth = _xmlPart.depth;
var _formatters = _xmlPart.formatters || [];
var _conditions = _xmlPart.conditions || [];
// keep highest position for the last xml part
if (_xmlPart.pos > _keepHighestPosition) {
_keepHighestPosition = _xmlPart.pos;
}
// create the xml part and set its absolute position in the xml
_code.add('main', '_xmlPos['+(_nbCustomIterator+_partDepth*2)+'] = '+_xmlPart.pos+';\n');
_code.add('main', '_strPart = {\n');
_code.add('main', " 'pos' : _xmlPos.slice(0, "+(_nbCustomIterator+_partDepth*2+1)+'),\n');
_code.add('main', " 'str' : ''\n");
_code.add('main', '};\n');
if (_xmlPart.before) {
_code.add('main', '_strPart.bef = '+_getXMLStrIndex(_xmlPart.before) + ';\n');
}
if (_xmlPart.array === 'start') {
_code.add('main', '_strPart.rowStart = true;\n');
}
else if (_xmlPart.array === 'end') {
_code.add('main', '_strPart.rowEnd = true;\n');
}
// insert the data only if it not null
if (_dataAttr) {
// handle conditions
_code.add('main', '_strPart.rowShow = true;\n');
_code.add('main', that.getFilterString(_getSafeVar, _getSafeValue, _conditions, '_strPart.rowShow = false', _getSafeVar(_objName), true));
_code.add('main', 'var _str = ' + _getSafeVar(_dataObj) + ' !== undefined && ' + _getSafeVar(_dataObj) + ' !== null ? ' + _getSafeVar(_dataObj) + '[' + _getSafeValue(_dataAttr) + ']' + ' : undefined ;\n');
// TODO optimize avoid using all this options for all formatters
_code.add('main', 'context.stopPropagation = false;\n');
_code.add('main', 'context.isConditionTrue = null;\n');
_code.add('main', 'context.isAndOperator = null;\n');
_code.add('main', 'context.isHidden = null;\n');
_code.add('main', 'context.parentsData = ['+_getSafeVar(_dataObj)+', '+_objParentNames.map(_getSafeVar).join(',')+'];\n');
_code.add('main', that.getFormatterString(_getSafeValue, '_str', 'context', _formatters, existingFormatters, false));
// replace null or undefined value by an empty string
_code.add('main', 'if(_str === null || _str === undefined) {\n');
_code.add('main', " _str = '';\n");
_code.add('main', '};\n');
_code.add('main', 'if (context.isHidden !== null){\n');
_code.add('main', ' _strPart.hide = context.isHidden;\n');
_code.add('main', '}\n');
// escape special characters for xml if the data is a string
_code.add('main', "if(typeof(_str) === 'string') {\n");
_code.add('main', " _str = _str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/[\\u0000-\\u0008]|[\\u000B-\\u000C]|[\\u000E-\\u001F]/g, '');\n");
_code.add('main', '};\n');
// insert formatters which can inject XML, so after .replace(/</g, '<') ... etc
_code.add('main', that.getFormatterString(_getSafeValue, '_str', 'context', _formatters, existingFormatters, true));
_code.add('main', "_strPart.str += (_strPart.rowShow !== false)?_str:''"+';\n');
}
if (_xmlPart.after) {
_code.add('main', '_strPart.aft = ' + _getXMLStrIndex(_xmlPart.after) + ';\n');
}
// push the xml part in a array (this array will be sorted at the end)
// add this if there is an iterator ( template is using i+1 form )
if ( _noIteratorTemplate === false ) {
_code.add('main', '_strParts.push(_strPart);\n');
// when no iterator only push when the part position is new or rowshow is true
}
else {
_code.add('main', 'if(_registeredXml['+_xmlPart.pos+'] !== true || _strPart.rowShow === true ) {\n');
_code.add('main', ' _strParts.push(_strPart);\n');
_code.add('main', ' _registeredXml['+_xmlPart.pos+'] = true;\n');
_code.add('main', '};\n');
}
}
}
// close previously created for-loops
while (_nestedArray.length > 0) {
_code.add('prev', 'main', '}\n');
_nestedArray.pop();
}
if (_staticData.after !== '') {
_code.add('main', '_strPart = {\n');
_code.add('main', " 'pos' : ["+(_keepHighestPosition+1)+'],\n');
_code.add('main', " 'str' : '',\n");
_code.add('main', " 'aft' : "+ _getXMLStrIndex(_staticData.after) + '\n');
_code.add('main', '};\n');
_code.add('main', '_strParts.push(_strPart);\n');
}
_code.add('main', 'return _strParts;');
var _codeAssembled = _code.init;
if (_atLeastOneSpecialIterator === true) {
_codeAssembled += _code.prev;
}
_codeAssembled += _code.main;
// The function is built, we compile it and check errors in the same time
var _fn;
try {
_fn = new Function('data', 'context', 'helper', _dictionaryName, _codeAssembled);
_fn.builderDictionary = _safeAccessor.getDictionary();
}
catch (err) {
console.log('Execution plan error; ' + err + '\n' + _codeAssembled);
throw new Error('Impossible to prepare execution plan. Please, contact the support');
}
return _fn;
}
};
/** ***************************************************************************************************************/
/* Privates methods */
/** ***************************************************************************************************************/
module.exports = builder;
/**
* Test if two arrays are equal
* @param {Array} arrA
* @param {Array} arrB
* @return {Boolean} true if the two arrays are equals, false otherwise
*/
function isArrayEqual (arrA, arrB) {
var _arrALength = arrA.length;
var _arrBLength = arrB.length;
if (_arrALength !== _arrBLength) {
return false;
}
for (var i = _arrALength - 1; i >= 0; i--) {
if (arrA[i] !== arrB[i]) {
return false;
}
}
return true;
}
/**
* Add an element in the array if does not already exist
* @param {Array} arr array
* @param {Mixed} value value
*/
function addIfNotExist (arr, value) {
for (var i = 0; i < arr.length; i++) {
var _value = arr[i];
if (_value === value) {
return;
}
}
arr.push(value);
}
/**
* Find and remove the element of the array
* @param {Array} arr array
* @param {Mixed} value value
* @return {Array} array modified
*/
function removeFrom (arr, value) {
for (var i = 0; i < arr.length; i++) {
if (arr[i]===value) {
arr.splice(i, 1);
return arr;
}
}
return arr;
}