carbone
Version:
Fast, Simple and Powerful report generator. Injects JSON and produces PDF, DOCX, XLSX, ODT, PPTX, ODS, ...!
841 lines (805 loc) • 32.6 kB
JavaScript
var helper = require('./helper');
var condition = require('../formatters/condition');
const MARKER_PRESENCE_CHAR = '\uFFFF';
var parser = {
/**
* Determines whether the specified one marker is a carbone marker.
*
* @param {String} oneMarker One marker
*/
isCarboneMarker : function (oneMarker) {
const _isCarboneMarkerRegexp = /^\{?\s*(?:[cdt]\s*[.[:(])|^\{?\s*[#$]|^\{?\s*bindColor/;
return _isCarboneMarkerRegexp.test(oneMarker);
},
/**
* Find all markers in the xml.
* All markers are declared with braces like this: {d.title}.
* It returns an object which contains two things:
* - a cleaned xml string where the markers are removed
* - an array of markers with their position in the cleaned xml
*
* @param {string} xml : xml string
* @param {Function} callback(err, cleanedXml, markers) where "markers" is an array like this [{pos:5, name:'title'}]
*/
findMarkers : function (xml, callback) {
var that = this;
var _allMarkers = [];
var _previousMarkerLength = 0;
var _previousMarkerPos = -1;
var _nbMarkerPresenceChar = 0;
var _markerPresenceChar = MARKER_PRESENCE_CHAR;
var _cleanedXml = xml;
// Capture { } markers again and extract them
_cleanedXml = _cleanedXml.replace(/\{([^{]+?)\}/g, function (m, marker, offset) {
if (that.isCarboneMarker(marker) === false) {
return m;
}
var _cleanedMarker = that.removeWhitespace(that.cleanMarker(marker));
_markerPresenceChar = '';
if (condition._isConditionalBlockEndMarker(_cleanedMarker) === false && condition._isConditionalBlockBeginMarker(_cleanedMarker) === false) {
// Replace the marker by a special character for all markers, except begin/end conditional blocks. This character is removed after.
// This is the simplest method to manage this case "<p>{d.id}{d.id:showBegin}a</p>{d.id:showEnd}" -> "<p>\uFFFFa</p>"
// It prevents the if-block system deleting the whole paragraph "<p></p>" when there is a marker just after <p>
++_nbMarkerPresenceChar;
_markerPresenceChar = MARKER_PRESENCE_CHAR;
}
var _pos = offset - _previousMarkerLength + _nbMarkerPresenceChar;
if (_pos === Math.trunc(_previousMarkerPos)) {
// Avoid exactly the same XML position to sort all parts at the end of the process
// Why adding 1/64? to avoid rounding problems of floats (http://0.30000000000000004.com)
// It let us the possibility to have 64 markers at the same position. It should be enough for all cases.
_pos = _previousMarkerPos + 1/64;
}
var _obj = {
pos : _pos,
name : '_root.'+_cleanedMarker
};
_allMarkers.push(_obj);
_previousMarkerLength += (marker.length + 2); // 2 equals the number of braces '{' or '}'
_previousMarkerPos = _pos;
return _markerPresenceChar;
});
process.nextTick(function () {
callback(null, _cleanedXml, _allMarkers);
});
},
/**
* @description Remove XML tags inside markers
* @param {String} xml
* @returns {String} Return a clean XML
*/
removeXMLInsideMarkers : function (xml) {
if (!xml || typeof xml !== 'string') {
return xml;
}
var _cleanedXml = xml.replace(/(\r\n|\n|\r)/g,' ');
// Markers inside xml tag break the system which find markers because we can have nested markers.
// So we find all xml tags and replace { } which are inside xml by a temporary character
_cleanedXml = _cleanedXml.replace(/((?:<[^>]+>)+)/g, function (m, xmlTag) {
// \u0000 and \uFFFF are forbidden character in XML so we should not have any conflict with other characters
return xmlTag.replace(/\{/g, '\u0000').replace(/\}/g, '\uFFFF');
});
// Capture { } markers and remove XML inside markers
_cleanedXml = _cleanedXml.replace(/(\{[^{]+?\})/g, function (m, markerWithXml) {
// is this marker contains xml? like this {d. <xml> <tr> menu} </xml> </tr>
if (/</.test(markerWithXml) === true) {
let _markerOnly = parser.extractMarker(markerWithXml);
let _xmlOnly = parser.cleanXml(markerWithXml);
// verify if it is really a Carbone marker, otherwise do nothing
if (parser.isCarboneMarker(_markerOnly) === false) {
return m;
}
// put the conditional block beginEnd/hideEnd markers at the end of the brace to remove everything in-between
if (condition._isConditionalBlockEndMarker(_markerOnly) === true) {
return _xmlOnly + _markerOnly;
}
// separate clearly the marker and the xml
return _markerOnly + _xmlOnly;
}
return m;
});
// put back markers inside XML
// eslint-disable-next-line no-control-regex
_cleanedXml = _cleanedXml.replace(/\u0000/g, '{').replace(/\uFFFF/g, '}');
return _cleanedXml;
},
/**
* Extract markers from the xml
*
* @param {string} markerStr : xml with marker
* @return {string} marker without xml
*/
extractMarker : function (markerStr) {
// "?:" avoiding capturing. otther method /\s*<([^>]*)>\s*/g
var _res = markerStr
.replace(/<(?:[\s\S]+?)>/g, function () {
return '';
});
return _res;
},
/**
* Remove extra whitespaces and remove special characters in the markers
*
* @param {string} markerStr : polluted marker string with whitespaces
* @return {string} cleaned marker string
*/
cleanMarker : function (markerStr) {
var _res = markerStr
// Remove special characters
.replace(/[\n\t]/g, '')
// Replace encoded "<" and ">" by non encoded characters
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, '\'');
return _res;
},
/**
* Replace encoded operator characters by no encoded characters
* @param {String} str
* @return {String} string with decoded characters
*/
replaceEncodedOperatorCharactersByNoEncodedCharacters : function (str) {
var _res = str
.replace(/&/g, '&')
.replace(/'/g, '\'')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/¦/g, '¦');
return _res;
},
/**
* Replace no encoded operator characters by encoded characters
* @param {String} str
* @return {String} string with encoded characters
*/
replaceNoEncodedOperatorCharactersByEncodedCharacters : function (str) {
var _res = str
.replace(/&/g, '&')
.replace(/'/g,''')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/¦/g, '¦');
return _res;
},
/**
* Remove markers, get only xml
*
* @param {string} xml : xml string with markers. Ex: menu[<tr>i</xml>]
* @return {string} only xml without marker. Ex: <tr></xml>
*/
cleanXml : function (xml) {
var _res = '';
xml.replace(/\s*(<[^>]*>)\s*/g, function (m, text) {
_res += text;
return '';
});
return _res;
},
/**
* Remove whitespace in a string except between quotes
* @param {String} str text to parse
* @return {String} text without whitespace
*/
removeWhitespace : function (str) {
var _res = '';
// do we need to manage escaped quotes ? /([^"]+)|("(?:[^"\\]|\\.)+")/
if (str) {
_res = str.replace(/([^"^']+)|(["|'](?:[^"'\\]|\\.)+["|'])/g, function (m, strWithoutQuotes, strWithQuotes) {
if (strWithoutQuotes) {
return strWithoutQuotes.replace(/\s/g, '');
}
else {
return strWithQuotes;
}
});
return _res;
}
return str;
},
/**
* Parse the xml, select the {t()} marker and translate them by using the corresponding translation
* @param {String} xml
* @param {Object} options {
* 'lang' : selected lang: 'fr',
* 'translations' : all translations: {fr: {}, en: {}, es: {}}
* }
* @param {Function} callback Optional callback (err, xmlTranslated) : xml with all marker {t()} replaced by the traduction
*/
translate : function (xml, options, callback) {
var _translations = options.translations !== undefined ? options.translations[options.lang] : {};
if (typeof(xml) !== 'string') {
return callback ? callback(null, xml) : xml;
}
// capture {t } markers. The part "(?:\s*<[^>]*>\s*)*" is used to eleminate xml tags
var _cleanedXml = xml.replace(/\{\s*((?:\s*<[^>]*>\s*)*t[\s\S]+?)\}/g, function (m, text) {
var _xmlOnly = parser.cleanXml(text);
// capture words inside () in order to translate them with the correct lang.json.
var _pattern = /\((.*)\)/.exec(text);
/* If there is an expression inside ()
_pattern contains [_cleanMarkerStr without 't', words inside (), index, _cleanMarkerStr] */
if (_pattern instanceof Array && _pattern.length > 1) {
// If _pattern[1] exist, we translate the expression. Else we write _pattern[1]
var _strToTranslate = parser.extractMarker(_pattern[1]);
// decode encoded characters
_strToTranslate = parser.replaceEncodedOperatorCharactersByNoEncodedCharacters(_strToTranslate);
var _translatedStr = _strToTranslate;
// if the translation exists and different of null, translate it.
if (_translations !== undefined && _translations[_strToTranslate] !== undefined && _translations[_strToTranslate] !== '') {
_translatedStr = _translations[_strToTranslate];
}
_translatedStr = parser.replaceNoEncodedOperatorCharactersByEncodedCharacters(_translatedStr);
return _translatedStr + _xmlOnly;
}
else {
return m;
}
});
return callback ? callback(null, _cleanedXml) : _cleanedXml;
},
/**
* Find declared variables in xml
* @param {String} xml xml document
* @param {Array} existingVariables existing variables
* @param {Function} callback(err, cleanedXml, variables)
*/
findVariables : function (xml, existingVariables, callback) {
if (typeof(existingVariables) === 'function') {
callback = existingVariables;
existingVariables = [];
}
if (existingVariables === undefined) {
existingVariables = [];
}
if (typeof(xml) !== 'string') {
return callback(null, '', []);
}
// capture {# } markers. The part "(?:\s*<[^>]*>\s*)*" is used to eleminate xml tags
var _cleanedXml = xml.replace(/\{\s*((?:\s*<[^>]*>\s*)*#[\s\S]+?)\}/g, function (m, text) {
var _marker = parser.extractMarker(text);
var _xmlOnly = parser.cleanXml(text);
var _variableStr = parser.cleanMarker(_marker);
// find pattern #myVar($a,$b) menu[$a+$b]
var _pattern = /#\s*([\s\S]+?)\s*(?:\(([\s\S]+?)\))?\s*=\s*([\s\S]+?)$/g.exec(_variableStr);
if (_pattern instanceof Array) {
var _variable = parser.removeWhitespace(_pattern[1]);
var _paramStr = parser.removeWhitespace(_pattern[2]);
var _code = parser.removeWhitespace(_pattern[3]);
// if the variable is a function with parameters
if (_paramStr !== undefined) {
// separate each parameters
var _params = _paramStr.split(/ *, */);
var _sortedParams = [];
// sort all params, longest params first to avoid a problem when two variable begin with the same word
for (var i = 0; i < _params.length; i++) {
_sortedParams.push({index : i, name : _params[i]});
}
_sortedParams.sort(function (a,b) {
return b.name.length - a.name.length;
});
// replace all parameters by _$0_, _$1_, _$2_, ..., in the code
for (var p = 0; p < _sortedParams.length; p++) {
var _param = _sortedParams[p];
_code = _code.replace(new RegExp('\\'+_param.name,'g'), '_$'+_param.index+'_');
}
}
var _regex = new RegExp('\\$'+_variable+'(?!\\w)(?:\\(([\\s\\S]+?)\\))?', 'g');
existingVariables.push({
name : _variable,
code : _code,
regex : _regex
});
return _xmlOnly;
}
else {
return m;
}
});
process.nextTick(function () {
callback(null, _cleanedXml, existingVariables);
});
},
/**
* Detect used variables and update all markers accordingly (replace variables by their values)
* @param {Array} markers : all markers of the document
* @param {Array} variables : all declared variables of the document
*/
preprocessMarkers : function (markers, variables, callback) {
if (variables instanceof Array && variables.length > 0) {
// preprocess all markers
for (var i = 0; i < markers.length; i++) {
var _marker = markers[i];
var _markerName = _marker.name;
// Does the marker contain a variable?
if (_markerName.indexOf('$') !== -1) {
// check if it matches with one previously declared variable
for (var j=0; j < variables.length; j++) {
var _variable = variables[j];
var _pattern = _variable.regex.exec(_markerName);
while (_pattern !== null) {
var _code = _variable.code;
var _varStr = _pattern[0];
var _paramStr = _pattern[1];
// if there some parameters?
if (_paramStr !== undefined) {
// separate each parameters
var _params = _paramStr.split(/ *, */);
// replace all parameters _$0_, _$1_, _$2_, ..., by their real values
for (var p = 0; p < _params.length; p++) {
var _param = _params[p];
_code = _code.replace(new RegExp('_\\$'+p+'_','g'), _param);
}
}
_marker.name = helper.replaceAll(_marker.name, _varStr, _code);
_pattern = _variable.regex.exec(_markerName);
}
}
}
}
}
parser.assignLoopId(markers);
callback(null, markers);
},
/**
* Assign loop IDs for count formatter
*
* @param {Array} markers Array of markers
* @return {Array} Array of markers with loop ID in count parameters
*/
assignLoopId : function (markers) {
var _loopWithRowNumberRegex = /.*?:count(\((.*?)\))?/g;
var match;
for (var _key in markers) {
var _marker = markers[_key];
// If the marker has a count formatter
match = _loopWithRowNumberRegex.exec(_marker.name);
if (match) {
var _parameters = '()';
var _before = '(';
var _after = ', ' + match[2] + ')';
var _loopId = _key + _marker.pos;
// If parameters are given, we store it
if (match[1]) {
_parameters = match[1];
}
// If no parameters are given, _after is set to )
if (match[2] === undefined || match[2] === '') {
_after = ')';
}
// Now we can concatenate _before, _loopId and _after
// _after are the given parameters
_parameters = _parameters.replace(/\(.*?\)/, _before + _loopId + _after);
// And we can replace _marker.name by the new one
_marker.name = _marker.name.replace(/count(\(.*?\))?/, 'count' + _parameters);
}
}
},
/**
* Find position of the opening tag which matches with the last tag of the xml string
*
* @param {string} leftSideXml : xml string
* @param {integer} indexWhereToStopSearch (optional) : force the algorithm to find the opening tag before this position
* @return {integer} index position in the xml string
*/
findOpeningTagPosition : function (leftSideXml, indexWhereToStopSearch) {
indexWhereToStopSearch = indexWhereToStopSearch || leftSideXml.length;
var _tagCount = 0;
var _prevTagPosEnd = 0;
var _openingTagIndex = {};
var _lastOpeningTagIndexBeforeStop = {};
var _similarTag = new RegExp('<(/)?[^>]+?(/)?>', 'g');
var _tag = _similarTag.exec(leftSideXml);
while (_tag !== null) {
if (_tag[1]==='/') {
_tagCount--;
}
else {
if (_prevTagPosEnd<indexWhereToStopSearch) {
_lastOpeningTagIndexBeforeStop[_tagCount] = (_tag.index >= indexWhereToStopSearch)? indexWhereToStopSearch-1 : _tag.index;
}
_openingTagIndex[_tagCount] = _tag.index;
// eliminate self-closing tag
if (_tag[2] !== '/') {
_tagCount++;
}
}
_prevTagPosEnd = _tag.index + _tag[0].length;
_tag = _similarTag.exec(leftSideXml);
}
if (_openingTagIndex[_tagCount] === undefined) {
return 0;
}
else if (_openingTagIndex[_tagCount] < indexWhereToStopSearch) {
return _openingTagIndex[_tagCount];
}
else {
return (_lastOpeningTagIndexBeforeStop[_tagCount]!==undefined)? _lastOpeningTagIndexBeforeStop[_tagCount] : 0 ;
}
},
/**
* Find position of the closing tag which matches with the opening tag at the beginning of the xml string
*
* @param {string} rightSideXml : xml string
* @param {integer} indexWhereToStartSearch (optional) : force the algorithm to find the opening tag after this position
* @return {integer} index position in the xml string
*/
findClosingTagPosition : function (rightSideXml, indexWhereToStartSearch) {
indexWhereToStartSearch = indexWhereToStartSearch || 0;
var _startTagCount = 0;
var _endTagCount = 0;
var _endTagPos = -1;
var _similarTag = new RegExp('<(/)?[^>]+?(/)?>', 'g'); // reset the regex
var _tag = _similarTag.exec(rightSideXml);
while (_tag !== null) {
if (_tag[1]==='/') {
_endTagCount++;
}
else {
// eliminate self-closing tag
if (_tag[2] !== '/') {
_startTagCount++;
}
}
if (_endTagCount === _startTagCount) {
_endTagPos = _tag.index + _tag[0].length;
if (_endTagPos > indexWhereToStartSearch) {
break;
}
}
_tag = _similarTag.exec(rightSideXml);
}
if (_endTagPos > indexWhereToStartSearch) {
return _endTagPos;
}
else {
return -1;
}
},
/**
* Find the xml tag which defines the transition between two repeated sections.
* Example: In HTML, the transition between two rows is "</tr><tr>". The first tag is the closing tag of the previous row,
* the second one is the opening tag of the next row. We call it "pivot".
*
* @param {string} partialXml : xml string which contains the transition
* @return {object} an object like this : {
* 'part1End' :{'tag':'tr', 'pos': 5 },
* 'part2Start':{'tag':'tr', 'pos': 5 }
* }.
*/
findPivot : function (partialXml) {
var _tagCount = 0;
var _prevTagCount = 0;
var _highestTags = [];
var _highestTagCount = 0;
var _hasAChanceToContainAtLeastOnePivot = false;
// capture all tags
var _tagRegex = new RegExp('<(/)?([^>]*)>?','g');
var _tag = _tagRegex.exec(partialXml);
if (_tag === null) {
// when there is no XML, return the end of the string as the pivot
return {
part1End : {tag : '', pos : partialXml.length, selfClosing : true},
part2Start : {tag : '', pos : partialXml.length, selfClosing : true}
};
}
while (_tag !== null) {
var _xmlAllAttributes = _tag[2] || '';
var _tagType = '';
if (_tag[1]==='/') {
_tagCount++;
_tagType = '>'; // closing
}
else {
_tagCount--;
_tagType = '<'; // opening
}
if (_xmlAllAttributes.endsWith('/')) {
_xmlAllAttributes = _xmlAllAttributes.slice(0, -1);
_tagCount++;
_tagType = 'x'; // self-closing
}
if (_tagCount > 0) {
_hasAChanceToContainAtLeastOnePivot = true;
}
if (_tagCount > _highestTagCount) {
_highestTagCount = _tagCount;
_highestTags = [];
}
if (_tagCount === _highestTagCount || _prevTagCount === _highestTagCount) {
var _spaceIndex = _xmlAllAttributes.indexOf(' ');
var _xmlAttribute = _spaceIndex === -1 ? _xmlAllAttributes : _xmlAllAttributes.substr(0, _spaceIndex);
var _tagInfo = {
tag : _xmlAttribute,
pos : _tag.index,
type : _tagType,
posEnd : partialXml.length // by default
};
// the end position of a tag equals the beginning of the next one
if (_highestTags.length>0) {
_highestTags[_highestTags.length-1].posEnd = _tagInfo.pos;
}
_highestTags.push(_tagInfo);
}
_prevTagCount = _tagCount;
_tag = _tagRegex.exec(partialXml);
}
if ( _tagCount !== 0 && (_highestTags.length===0 || (_highestTags.length===1 && _highestTags[0].type!=='x') || _highestTags[0].type==='<')) {
return null;
}
var _firstTag = _highestTags[0];
var _lastTag = _highestTags[_highestTags.length-1];
var _pivot = {
part1End : {tag : _firstTag.tag, pos : _firstTag.posEnd},
part2Start : {tag : _lastTag.tag, pos : _lastTag.pos }
};
if (_firstTag.type==='x') {
_pivot.part1End.selfClosing = true;
}
if (_lastTag.type==='x') {
_pivot.part2Start.selfClosing = true;
}
// exceptional case where there is only one self-closing tag which separate the two parts.
if (_highestTags.length===1) {
_pivot.part2Start.pos = _pivot.part1End.pos;
}
// TODO, this code could be simplified and generalized when there is no XML
// if it contains a flat XML structure, considers it is a selfClosing tag
if ( _tagCount === 0 && _hasAChanceToContainAtLeastOnePivot === false) {
_pivot = {
part1End : {tag : _lastTag.tag, pos : _lastTag.posEnd, selfClosing : true},
part2Start : {tag : _lastTag.tag, pos : _lastTag.posEnd, selfClosing : true}
};
}
return _pivot;
},
/**
* Find exact position in the XML of the repeated area (first, and second part)
*
* @param {string} xml : xml string of the repeated area
* @param {object} pivot : object returned by findPivot.
* @param {integer} roughStartIndex (optional) : forces the algorithm to find the beginning of the repetition before it
* @return {object} object which specify the start position and the end position of a repeated area.
*/
findRepetitionPosition : function (xml, pivot, roughStartIndex) {
var _that = this;
if (!pivot) {
return null;
}
// First part
var _leftSideXml = xml.slice(0, pivot.part1End.pos);
var _startTagPos = _that.findOpeningTagPosition(_leftSideXml, roughStartIndex+1);
// Second part
var _rightSideXml = xml.slice(pivot.part2Start.pos);
var _endTagPos = _that.findClosingTagPosition(_rightSideXml);
// if the closing tag is not found and this is a flat structure
if (_endTagPos === -1 && pivot.part2Start.selfClosing === true) {
_endTagPos = 0; // TODO should be the rough end of the array
}
return {startEven : _startTagPos, endEven : pivot.part2Start.pos, startOdd : pivot.part2Start.pos, endOdd : pivot.part2Start.pos + _endTagPos};
},
/**
* Transform XML in a flat array of objects
*
* @param {String} xml
* @return {Array}
*/
flattenXML : function (xml) {
var _array = [];
if (typeof (xml) !== 'string') {
return [];
}
var _xmlTagRegExp = /<([^>]+)>/g;
var _tag = _xmlTagRegExp.exec(xml);
var _depth = 0;
var _prevLastIndex = 0;
var _openingTagPos = [];
if (_tag === null) {
_array.push({
id : 0,
index : 0,
lastIndex : xml.length,
depth : 0
});
}
while (_tag !== null) {
if ( _prevLastIndex < _tag.index) {
// non XML part
_array.push({
id : _array.length,
index : _prevLastIndex,
lastIndex : _tag.index,
depth : _depth
});
}
var _xmlTag = {
id : _array.length,
index : _tag.index,
lastIndex : _xmlTagRegExp.lastIndex,
depth : _depth
};
// closing tag
if (_tag[1].startsWith('/') === true) {
_xmlTag.depth = _depth--;
var _correspondingOpenPos = _openingTagPos.pop();
if (_correspondingOpenPos === undefined) {
throw new Error('XML not valid');
}
_correspondingOpenPos.match = _xmlTag.id;
_xmlTag.match = _correspondingOpenPos.id;
}
// is not a self-closing tag, so it is an opening tag
else if (_tag[1].endsWith('/') === false) {
_xmlTag.depth = _depth++;
_xmlTag.match = -1; // temp
_openingTagPos.push(_xmlTag);
}
_array.push(_xmlTag);
_prevLastIndex = _xmlTagRegExp.lastIndex;
_tag = _xmlTagRegExp.exec(xml);
}
return _array;
},
/**
* Detect and move the if-block beginning and ending if possible
*
* For example "<p>{d.id:showBegin}blabla</p>{d.id:showEnd}" becomes "{d.id:showBegin}<p>blabla</p>{d.id:showEnd}"
* It pushes/pulls the begin/end beyond the limit in order to include surrounded XML tags
*
* @param {Array} xmlTree List of all XML tag, returned by parser.flattenXML
* @param {Integer} beginFlattenXMLIndex where is the beginTextIndex in the xmlTree
* @param {Integer} beginTextIndex char position of if-block's beginning
* @param {Integer} endTextIndex char position of if-block's ending
* @return {Object} new begin/end position for if-block
*/
moveConditionalBlockBeginEnd : function (xmlTree, beginFlattenXMLIndex, beginTextIndex, endTextIndex) {
const _dummyObject = {}; // constant, empty object to avoid instanciation in the loop below
var _newBeginTreeIndex = beginFlattenXMLIndex;
var _newBeginTextIndex = beginTextIndex;
var _newEndTextIndex = endTextIndex;
for (var i = beginFlattenXMLIndex; i < xmlTree.length; i++) {
var _tag = xmlTree[i];
var _matchingTag = _tag.match > -1 ? xmlTree[_tag.match] : _dummyObject;
// if this XML part is included in the if-block, and if it is an opening tag
if (_matchingTag.lastIndex < endTextIndex && _tag.id < _matchingTag.id) {
// jump directly to the end of this currently visited XML (optimization)
i = _matchingTag.id;
}
// if we find a corresponding opening tag next to the first tag of the if-block
// move the if-block's beginning earlier
if (_matchingTag.lastIndex === _newBeginTextIndex) {
_newBeginTextIndex = _matchingTag.index;
_newBeginTreeIndex = _matchingTag.id;
}
// if we find a corresponding ending tag next to the last tag of the if-block
// move the if-block's ending further
if (_tag.index >= endTextIndex && _matchingTag.index < _tag.index && _matchingTag.index >= beginTextIndex ) {
_newEndTextIndex = _tag.lastIndex;
}
if (_tag.lastIndex > _newEndTextIndex || (_matchingTag.lastIndex < _newBeginTextIndex && _tag.index > _newEndTextIndex)) {
break;
}
}
if (_newBeginTreeIndex !== beginFlattenXMLIndex) {
_newBeginTextIndex = xmlTree[_newBeginTreeIndex].index;
}
return [_newBeginTreeIndex, _newBeginTextIndex, _newEndTextIndex];
},
/**
* Find safe position of the conditional block in XML
*
* Safe means "does not break XML if the XML block between beginning and end is removed"
*
* @param {String} xmlFlattened flattened XML returned by flattenXML
* @param {Integer} startSearchIndex beginning index of the conditional block
* @param {Integer} endSearchIndex ending index of the conditional block
* @return {Array} [newBegining, newEnding]
*/
findSafeConditionalBlockPosition : function (xmlFlattened, beginTextIndex, endTextIndex) {
beginTextIndex = Math.trunc(beginTextIndex);
endTextIndex = Math.trunc(endTextIndex);
var _validCandidates = [];
var _newCandidate = null;
var _lastCandidate = [];
const _dummyObject = {}; // constant, empty object to avoid instanciation in the loop below
// TODO: we can improve performance of we store beginFlattenXMLIndex for each XML part
// We could avoid this loop for each call of findSafeConditionalBlockPosition
var _beginFlattenXMLIndex = 0;
while (_beginFlattenXMLIndex < xmlFlattened.length) {
if (xmlFlattened[_beginFlattenXMLIndex].lastIndex > beginTextIndex) {
break;
}
_beginFlattenXMLIndex++;
}
// find better begin/end conditional blocks
var _newBeginEndValues = parser.moveConditionalBlockBeginEnd(xmlFlattened, _beginFlattenXMLIndex, beginTextIndex, endTextIndex);
_beginFlattenXMLIndex = _newBeginEndValues[0];
beginTextIndex = Math.trunc(_newBeginEndValues[1]);
endTextIndex = Math.trunc(_newBeginEndValues[2]);
var _tag = xmlFlattened[_beginFlattenXMLIndex] || {};
while (_beginFlattenXMLIndex < xmlFlattened.length) {
if (_tag.index >= endTextIndex) {
break;
}
var _matchingTag = _tag.match > -1 ? xmlFlattened[_tag.match] : _dummyObject;
_beginFlattenXMLIndex++;
// if this XML part is included in the if-block, and if it is an opening tag
if (_matchingTag.lastIndex <= endTextIndex && _tag.id < _matchingTag.id) {
_newCandidate = [_tag.index, _matchingTag.lastIndex];
// jump directly to the next valid XML part
_beginFlattenXMLIndex = _matchingTag.id + 1;
}
// no XML
else if (_tag.match === undefined) {
_newCandidate = [_tag.index, _tag.lastIndex];
}
if (_newCandidate !== null) {
// merge adjacent valid XML part
if (_newCandidate[0] === _lastCandidate[1]) {
_lastCandidate[1] = _newCandidate[1];
}
else if (_newCandidate[1] === _lastCandidate[0]) {
_lastCandidate[0] = _newCandidate[0];
}
else {
// or add new part
_validCandidates.push(_newCandidate);
_lastCandidate = _newCandidate;
}
_newCandidate = null;
}
_tag = xmlFlattened[_beginFlattenXMLIndex];
}
// edge case: when there is no XML tags, return directly
if (xmlFlattened.length === 0) {
return [[beginTextIndex, endTextIndex]];
}
if (_validCandidates.length === 0) {
return [[endTextIndex, endTextIndex]];
}
// never go beyond limits
if (_lastCandidate[1] > endTextIndex) {
_lastCandidate[1] = endTextIndex;
}
if (_validCandidates[0][0] < beginTextIndex) {
_validCandidates[0][0] = beginTextIndex;
}
return _validCandidates;
},
/**
* Simple mathematical expression parser without parenthesis
*
* @param {String} mathExpr The mathematics expression coming from a formatter calc, add, mul, div, sub
* @param {Function} safeVariableInjectionFn The safe variable injection function
* @return {String} code to inject in builder
*/
parseMathematicalExpression : function (mathExpr, safeVariableInjectionFn) {
if (typeof mathExpr !== 'string' || mathExpr.trim() === '') {
return '';
}
let _currMathExprIndex = mathExpr.length - 1;
let _injectedCode = '';
let _prevVariable = '';
let _prevOperator = '';
const _variables = mathExpr.split(/[*+/\-]/);
for(let i = _variables.length - 1; i > -1; i--) {
const _variable = _variables[i];
_currMathExprIndex -= (_variable.length + 1); // add operator length, which is always "1"
const _operator = mathExpr[_currMathExprIndex + 1] ?? '';
const _trimVariable = _variable.trim();
if (_trimVariable.length === 0 && _prevOperator !== '-') {
throw Error ('Bad Mathematical Expression in "'+mathExpr+'"');
}
// accept variable with dashes for example
if (/^[0-9\.]/.test(_trimVariable) === false) {
_prevVariable = _operator + _trimVariable + _prevVariable;
continue;
}
const _fullVariable = _trimVariable + _prevVariable;
const _safeVarCode = safeVariableInjectionFn(_fullVariable);
_prevVariable = '';
_injectedCode = _operator + 'parseFloat(' + _safeVarCode + ')' + _injectedCode;
_prevOperator = _operator;
}
return _injectedCode;
}
};
module.exports = parser;