Comparing version 1.0.1 to 1.1.0
@@ -0,1 +1,24 @@ | ||
### v1.1.0 | ||
- Release February 26, 2018 | ||
- Fix: should find markers even if there is a opening bracket `{` before the markers | ||
- Fix: accept to nest arrays in XML whereas these arrays are not nested in JSON | ||
- Fix: markers were not parsed if formatters were used directly on `d` or `c` like this `{d:ifEmpty('yeah')}` ... | ||
- Fix: keep the first element of the array if the custom iterator is constant | ||
- Fix a lot of strange bugs when using a filter without iterators in arrays (ex. `{d.cities[i=0].temperature}`) | ||
- Optimization: gain x10 when sorting 1 Million of rows | ||
- Add formatter `unaccent` to remove accent from string | ||
- `carbone.set` does not overwrite user-defined translations | ||
- Accepts iteration on non-XML. Example: `{d[i].brand} , {d[i+1].brand}` | ||
- Add new formatters | ||
- `unaccent()` to remove accent from string | ||
- `count()` to print a counter in loops. Usage: `{d[i].name:count()}` | ||
- `convCRLF()` to convert text, which contains `\r\n` or `\n`, into "real" carriage return in odt or docx document | ||
- Formatters which have the property `canInjectXML = true` can inject XML in documents | ||
- Return an error in render callback when LibreOffice is not detected | ||
- Get the last object of an array using negative values when filtering with `i` iterator | ||
- `{d.cities[i=-1].temperature}` shows the temperature (if the array is not empty) of the last city | ||
- `{d.cities[i=-2].temperature}` shows the temperature of the city before the last | ||
- ... | ||
### V1.0.1 | ||
@@ -2,0 +25,0 @@ - Release October 13, 2017 |
@@ -88,5 +88,21 @@ | ||
/** | ||
* Count and print row (or column) number | ||
* | ||
* @param {String} d Array passed by carbone | ||
* @param {Integer} loopId Loop ID generated by carbone | ||
* @param {String} start Number to start with (default: 1) | ||
* @return {String} Counter value | ||
*/ | ||
function count (d, loopId, start) { | ||
if (start === undefined) { | ||
start = 1; | ||
} | ||
return '__COUNT_' + loopId + '_' + start + '__'; | ||
} | ||
module.exports = { | ||
arrayJoin : arrayJoin, | ||
arrayMap : arrayMap | ||
arrayMap : arrayMap, | ||
count : count | ||
}; |
@@ -0,1 +1,7 @@ | ||
const LINEBREAK = { | ||
odt : '<text:line-break/>', | ||
docx : '</w:t><w:br/><w:t>' | ||
}; | ||
/** | ||
@@ -123,2 +129,50 @@ * Lower case all letters | ||
/** | ||
* Removes accents from text | ||
* | ||
* @example [ "crème brulée" ] | ||
* @example [ "CRÈME BRULÉE" ] | ||
* @example [ "être" ] | ||
* @example [ "éùïêèà" ] | ||
* | ||
* @param {String} d string to parse | ||
* @return {String} string without accent | ||
*/ | ||
function unaccent (d) { | ||
if (typeof d === 'string') { | ||
return d.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); | ||
} | ||
return d; | ||
} | ||
/** | ||
* Convert carriage return `\r\n` and line feed `\n` to XML-specific code in rendered document | ||
* | ||
* Compatible with odt, and docx (beta) | ||
* | ||
* @exampleContext { "extension" : "odt" } | ||
* @example [ "my text \n contains Line Feed" , "my text <text:line-break/> contains Line Feed" ] | ||
* @example [ "my text \r\n contains Carriage Return" , "my text <text:line-break/> contains Line Feed" ] | ||
* | ||
* @exampleContext { "extension" : "docx" } | ||
* @example [ "my text \n contains Line Feed" , "my text </w:t><w:br/><w:t> contains Line Feed" ] | ||
* @example [ "my text \r\n contains Carriage Return" , "my text </w:t><w:br/><w:t> contains Line Feed" ] | ||
* | ||
* @param {Integer|String} d | ||
* @return {String} return "XML carriage return" for odt and docx | ||
*/ | ||
function convCRLF (d) { | ||
if (typeof d === 'string') { | ||
var _lineBreak = LINEBREAK[this.extension]; | ||
if (_lineBreak) { | ||
return d.replace(/\r?\n/g, _lineBreak); | ||
} | ||
} | ||
return d; | ||
} | ||
// this formatter is separately to inject code | ||
convCRLF.canInjectXML = true; | ||
module.exports = { | ||
@@ -130,3 +184,5 @@ lowerCase : lowerCase, | ||
convEnum : convEnum, | ||
convCRLF : convCRLF, | ||
unaccent : unaccent, | ||
print : print | ||
}; |
var extracter = require('./extracter'); | ||
var parser = require('./parser'); | ||
var helper = require('./helper'); | ||
var timSort = require('timsort'); | ||
@@ -17,2 +18,3 @@ var builder = { | ||
* 'existingVariables' : existing variables (Array returned by parser.findVariables()) | ||
* 'extension' : template file extension | ||
* } | ||
@@ -26,2 +28,3 @@ * @param {Function} callback | ||
} | ||
// find translation markers {t()} and translate | ||
@@ -70,7 +73,9 @@ parser.translate(xml, options, function (err, xmlWithoutTranslation) { | ||
* @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 (varName, contextName, formatters, existingFormatters) { | ||
getFormatterString : function (varName, contextName, formatters, existingFormatters, onlyFormatterWhichInjectXML) { | ||
var _lineOfCodes = []; | ||
var _ifClosingBraces = []; | ||
for (var i = 0; i < formatters.length; i++) { | ||
@@ -96,11 +101,13 @@ var _formatter = formatters[i]; | ||
} | ||
if (existingFormatters && existingFormatters[_functionStr] === undefined) { | ||
if (existingFormatters[_functionStr] === undefined) { | ||
var _alternativeFnName = helper.findClosest(_functionStr, existingFormatters); | ||
throw Error('Formatter "'+_functionStr+'" does not exist. Do you mean "'+_alternativeFnName+'"?'); | ||
} | ||
_lineOfCodes.push(varName +' = formatters.' + _functionStr + '.call(' + contextName + ', ' + varName + _argumentStr + ');\n'); | ||
_ifClosingBraces.push('}'); | ||
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 = formatters.length - 1; | ||
var _nbClosingBraces = _lineOfCodes.length - 1; | ||
while (_nbClosingBraces-- > 0) { | ||
@@ -115,3 +122,2 @@ _str += '}'; | ||
* | ||
* @param {string} objName : Example 'd.number' | ||
* @param {array} conditions : array of conditions. Example [{'left':'sort', 'operator':'>', 'right':'10'}] | ||
@@ -121,5 +127,6 @@ * @param {string} codeIfTrue : code to insert if the condition is true. Example 'row = true' | ||
* @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 (conditions, codeIfTrue, prefix, inverseCondition) { | ||
getFilterString : function (conditions, codeIfTrue, prefix, inverseCondition, forceObjectTested) { | ||
prefix = prefix || ''; | ||
@@ -138,2 +145,5 @@ if (!codeIfTrue || !(conditions instanceof Array) || conditions.length ===0) { | ||
var _objName = _condition.left.parent; | ||
if (forceObjectTested) { | ||
_objName = forceObjectTested; | ||
} | ||
// if the left part is an object | ||
@@ -144,9 +154,15 @@ var _objectSeparatorIndex = _attr.indexOf('.'); | ||
_attr = _attr.slice(_objectSeparatorIndex+1); | ||
_objName = '_'+prefix+_obj+'_filter'; | ||
if (_alreadyDeclaredFilter[_objName] === undefined) { | ||
_declareStr += 'var '+_objName+'='+_condition.left.parent+'["'+_obj+'"];\n'; | ||
var _subObjName = '_'+prefix+_obj+'_filter'; | ||
if (_alreadyDeclaredFilter[_subObjName] === undefined) { | ||
_declareStr += 'var '+_subObjName+'='+_objName+'["'+_obj+'"];\n'; | ||
} | ||
_objName = _subObjName; | ||
} | ||
if (_attr === 'i') { | ||
_str += _objName + '_i '+_condition.operator+_condition.right; | ||
var _rightOperator = _condition.right; | ||
var _arrayName = _condition.left.parent; | ||
if (parseInt(_condition.right, 10) < 0) { | ||
_rightOperator = _arrayName + '_array_length ' + _condition.right; | ||
} | ||
_str += _arrayName + '_i '+_condition.operator + _rightOperator; | ||
} | ||
@@ -180,7 +196,9 @@ else { | ||
var _res = ''; | ||
builder.sortXmlParts(arrayOfStringPart, sortDepth); | ||
builder.sortXmlParts(arrayOfStringPart); | ||
var _rowInfo = []; | ||
var _prevDepth = 0; | ||
var _prevPos = []; | ||
var _prevPart = {pos : []}; | ||
var _arrayLevel = 0; | ||
var _loopIds = {}; | ||
// TODO MANAGE sortDepth | ||
@@ -197,4 +215,7 @@ sortDepth = (sortDepth) ? sortDepth : 1; | ||
var _part = arrayOfStringPart[i]; | ||
if (isArrayEqual(_prevPos, _part.pos)===false) { | ||
_prevPos = _part.pos; | ||
// 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; | ||
} | ||
@@ -215,3 +236,3 @@ else { | ||
// TODO. Protection is sortDepth is too short | ||
// TODO. Protection if sortDepth is too short | ||
if (_rowInfo[_arrayLevel] === undefined) { | ||
@@ -239,2 +260,27 @@ _rowInfo[_arrayLevel] = { | ||
/** | ||
* 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. | ||
@@ -244,13 +290,11 @@ * INTERNAL USE ONLY (USED ONLY BY assembleXmlParts) | ||
* @param {array} arrayToSort : array of objects | ||
* @param {integer} sortDepth | ||
*/ | ||
sortXmlParts : function (arrayToSort, sortDepth) { | ||
if (sortDepth === undefined) { | ||
sortDepth = 1; | ||
} | ||
arrayToSort.sort(function (a, b) { | ||
sortXmlParts : function (arrayToSort) { | ||
timSort.sort(arrayToSort, function (a, b) { | ||
var i = 0; | ||
var _a = a.pos[i]; | ||
var _b = b.pos[i]; | ||
while (_a === _b && !(_a === undefined && _b === undefined) && i < sortDepth) { | ||
var _aLength = a.pos.length; | ||
var _bLength = b.pos.length; | ||
while (_a === _b && i < _aLength && i < _bLength) { | ||
i++; | ||
@@ -260,17 +304,9 @@ _a = a.pos[i]; | ||
} | ||
if (_a === undefined && _b === undefined && a.rowShow === true && b.rowShow === false) { | ||
return -1; | ||
if (_a !== undefined && _b !== undefined) { | ||
return (_a > _b) ? 1 : -1; | ||
} | ||
else if (_a === undefined && _b === undefined && a.rowShow === false && b.rowShow === true) { | ||
return 1; | ||
if (_a === undefined && _b === undefined && a.rowShow !== b.rowShow) { | ||
return (a.rowShow === false && b.rowShow === true) ? 1 : -1; | ||
} | ||
else if (_a === undefined) { | ||
return -1; | ||
} | ||
else if (_b === undefined) { | ||
return 1; | ||
} | ||
else { | ||
return (_a > _b) ? 1 : -1; | ||
} | ||
return _aLength - _bLength; | ||
}); | ||
@@ -300,2 +336,6 @@ }, | ||
var _lastArrayName = currentlyVisitedArrays[currentlyVisitedArrays.length-1]; | ||
// do not close current array loop if the next array is nested in the former | ||
if (objDependencyDescriptor[nextAttrName].type === 'array' && objDependencyDescriptor[nextAttrName].depth > objDependencyDescriptor[_lastArrayName].depth) { | ||
return; | ||
} | ||
while (_firstParentOfTypeArray !== _lastArrayName && currentlyVisitedArrays.length>0) { | ||
@@ -339,3 +379,3 @@ execute(_lastArrayName); | ||
_code.add('init', 'var _xmlPos = [0];\n'); | ||
_code.add('init', 'var _formatterOptions = { lang : options.lang, stopPropagation : false, enum : options.enum };\n'); | ||
_code.add('init', 'var _formatterOptions = { lang : options.lang, stopPropagation : false, enum : options.enum, extension : options.extension };\n'); | ||
_code.add('init', 'var formatters = options.formatters;\n'); // used by getFormatterString | ||
@@ -408,2 +448,15 @@ | ||
} | ||
else if (_type === 'objectInArray') { | ||
var _arrayOfObjectName = _objName+'_array'; | ||
var _arrayOfObjectIndexName = _objName+'_i'; | ||
_code.add('prev','main', 'var ' + _arrayOfObjectName+'='+'('+_objParentName+' !== undefined)? '+_objParentName+'[\''+_realObjName+'\']:[];\n'); | ||
var _conditionToFindObject = _dynamicData[_objName].conditions || []; | ||
var _objNameTemp = _objName+'_temp'; | ||
_code.add('prev', 'main', 'var '+_arrayOfObjectName+'_length = ('+_arrayOfObjectName+' instanceof Array)?'+_arrayOfObjectName+'.length : 0;\n'); | ||
_code.add('prev', 'main', 'for (var '+_arrayOfObjectIndexName+' = 0; '+_arrayOfObjectIndexName+' < '+_arrayOfObjectName+'_length ; '+_arrayOfObjectIndexName+'++) {\n'); | ||
_code.add('prev', 'main', ' var '+_objNameTemp+' = '+_arrayOfObjectName+'['+_arrayOfObjectIndexName+'];\n'); | ||
_code.add('prev', 'main', that.getFilterString(_conditionToFindObject, _objName+'='+_arrayOfObjectName+'['+_arrayOfObjectIndexName+'];\n break;\n', _objNameTemp, false, _objNameTemp)); | ||
_code.add('prev', 'main', '}\n'); | ||
} | ||
// Array type | ||
@@ -451,3 +504,3 @@ else if (_type === 'array') { | ||
if (_containSpecialIterator===true) { | ||
_code.add( 'main', ' | '+_missingIteratorArrayName+'.length>0'); | ||
_code.add( 'main', ' || '+_missingIteratorArrayName+'.length>0'); | ||
} | ||
@@ -538,3 +591,3 @@ _code.add('prev','main', ' ; '+_arrayIndexName+'++) {\n'); | ||
_code.add('main', '_formatterOptions.stopPropagation = false;\n'); | ||
_code.add('main', that.getFormatterString('_str', '_formatterOptions', _formatters, existingFormatters)); | ||
_code.add('main', that.getFormatterString('_str', '_formatterOptions', _formatters, existingFormatters, false)); | ||
// replace null or undefined value by an empty string | ||
@@ -548,2 +601,5 @@ _code.add('main', 'if(_str === null || _str === undefined) {\n'); | ||
_code.add('main', '};\n'); | ||
// insert formatters which can inject XML, so after .replace(/</g, '<') ... etc | ||
_code.add('main', that.getFormatterString('_str', '_formatterOptions', _formatters, existingFormatters, true)); | ||
_code.add('main', "_strPart.str += (_strPart.rowShow !== false)?_str:''"+';\n'); | ||
@@ -559,3 +615,3 @@ _code.add('main', '}\n'); | ||
_code.add('main', '_strParts.push(_strPart);\n'); | ||
// when no iterator only push when the part position is new or rowshow is true | ||
// when no iterator only push when the part position is new or rowshow is true | ||
} | ||
@@ -621,3 +677,3 @@ else { | ||
} | ||
for (var i = 0; i < _arrALength; i++) { | ||
for (var i = _arrALength - 1; i >= 0; i--) { | ||
if (arrA[i] !== arrB[i]) { | ||
@@ -624,0 +680,0 @@ return false; |
@@ -19,2 +19,3 @@ var path = require('path'); | ||
var isLibreOfficeFound = false; | ||
@@ -60,5 +61,5 @@ var converterOptions = { | ||
} | ||
// restart Factory automatically if it crashes. | ||
// restart Factory automatically if it crashes. | ||
isAutoRestartActive = true; | ||
// if we must start all factory now | ||
@@ -82,3 +83,3 @@ if (params.startFactory === true) { | ||
else { | ||
// else, start LibreOffice when needed | ||
// else, start LibreOffice when needed | ||
if (callback) { | ||
@@ -131,3 +132,3 @@ callback(); | ||
/** | ||
* Convert a document | ||
* Convert a document | ||
* | ||
@@ -137,5 +138,10 @@ * @param {string} inputFile : absolute path to the source document | ||
* @param {string} formatOptions : options passed to convert | ||
* @param {function} callback : function(buffer) result | ||
* @param {function} callback : function(buffer) result | ||
*/ | ||
convertFile : function (inputFile, outputType, formatOptions, callback, returnLink, outputFile) { | ||
if (isLibreOfficeFound === false) { | ||
return callback('Cannot find LibreOffice. Document conversion cannot be used'); | ||
} | ||
var _output = helper.getUID(); | ||
@@ -188,3 +194,3 @@ var _job = { | ||
var _uniqueName = helper.getUID(); | ||
// generate a unique path to a fake user profile. We cannot start multiple instances of LibreOffice if it uses the same user cache | ||
@@ -245,3 +251,3 @@ var _userCachePath = path.join(params.tempPath, '_office_' + _uniqueName); | ||
return function (error) { | ||
var _processName = ''; | ||
var _processName = ''; | ||
var _otherThreadToKill = null; | ||
@@ -272,3 +278,3 @@ | ||
debug('process '+_processName+' of factory '+factoryID+' died ' + error); | ||
// if both processes Python and Office are off... | ||
@@ -282,3 +288,3 @@ if (_factory.pythonThread === null && _factory.officeThread === null) { | ||
else { | ||
_otherThreadToKill.kill('SIGKILL'); | ||
_otherThreadToKill.kill('SIGKILL'); | ||
} | ||
@@ -290,3 +296,3 @@ }; | ||
/** | ||
* Manage factory restart ot shutdown when a factory is completly off | ||
* Manage factory restart ot shutdown when a factory is completly off | ||
* @param {Object} factory factory description | ||
@@ -297,3 +303,3 @@ */ | ||
if (isAutoRestartActive === true) { | ||
// if there is an error while converting a document, let's try another time | ||
// if there is an error while converting a document, let's try another time | ||
var _job = factory.currentJob; | ||
@@ -315,4 +321,4 @@ if (_job) { | ||
// else if Carbone is shutting down and there is an exitCallback | ||
else { | ||
// delete office files synchronously (we do not care because Carbone is shutting down) when office is dead | ||
else { | ||
// delete office files synchronously (we do not care because Carbone is shutting down) when office is dead | ||
helper.rmDirRecursive(factory.userCachePath); | ||
@@ -433,12 +439,9 @@ if (factory.exitCallback) { | ||
if (fs.existsSync('/Applications/LibreOffice.app/Contents/' + _path + '/python') === true) { | ||
isLibreOfficeFound = true; | ||
converterOptions.pythonExecPath = '/Applications/LibreOffice.app/Contents/' + _path + '/python'; | ||
converterOptions.sofficeExecPath = '/Applications/LibreOffice.app/Contents/MacOS/soffice'; | ||
} | ||
else { | ||
debug('cannot find LibreOffice'); | ||
} | ||
} | ||
else if (process.platform === 'linux') { | ||
var directories = fs.readdirSync('/opt'); | ||
var isLibreOfficeFound = false; | ||
var patternLibreOffice = /libreoffice\d+\.\d+/; | ||
@@ -455,5 +458,2 @@ for (var i = 0; i < directories.length; i++) { | ||
} | ||
if (isLibreOfficeFound === false) { | ||
debug('cannot find LibreOffice. Document conversion cannot be used'); | ||
} | ||
} | ||
@@ -463,2 +463,6 @@ else { | ||
} | ||
if (isLibreOfficeFound === false) { | ||
debug('cannot find LibreOffice. Document conversion cannot be used'); | ||
} | ||
} | ||
@@ -473,2 +477,2 @@ | ||
module.exports = converter; | ||
module.exports = converter; |
var parser = require('./parser'); | ||
var helper = require('./helper'); | ||
@@ -34,2 +35,4 @@ var extracter = { | ||
var _conditions = []; | ||
var _isIteratorUsedInCurrentArray = false; | ||
var _conditionsToFindObjectInArray = []; | ||
// Loop on each char. | ||
@@ -71,2 +74,3 @@ // We do not use regular expressions because it is a lot easier like that. | ||
else if (_char==='[') { | ||
_isIteratorUsedInCurrentArray = false; | ||
_isArray = true; | ||
@@ -143,2 +147,5 @@ _uniqueMarkerName += _word; | ||
} | ||
else if (_word.length > 1 && (_char===',' || _char===']')) { | ||
_isIteratorUsedInCurrentArray = true; // +1 or not | ||
} | ||
// detect array iteration | ||
@@ -156,3 +163,2 @@ if (_word.substr(-2)==='+1') { | ||
} | ||
// if we exit the bracket of an array | ||
@@ -182,2 +188,23 @@ if (_char===']' && _isArray===true) { | ||
} | ||
if (_isIteratorUsedInCurrentArray === false) { | ||
var _filterUniqueName = ''; | ||
var h = _conditions.length - 1; | ||
for (; h >= 0 ; h--) { | ||
var _conditionToFindObject = _conditions[h]; | ||
_filterUniqueName += helper.cleanJavascriptVariable(_conditionToFindObject.left.attr + '__' + _conditionToFindObject.right); | ||
// consider only conditions of this array, thus only last rows which match with unique array name | ||
if (_conditionToFindObject.left.parent !== _uniqueMarkerName ) { | ||
break; | ||
} | ||
} | ||
// extract conditions to find this object | ||
_conditionsToFindObjectInArray = _conditions.slice(h + 1, _conditions.length); | ||
_conditions = _conditions.slice(0, h + 1); | ||
// generate a new object | ||
_uniqueMarkerName += _filterUniqueName; | ||
// correction of previously detected conditions | ||
for (var m = 0; m < _conditionsToFindObjectInArray.length ; m++) { | ||
_conditionsToFindObjectInArray[m].left.parent += _filterUniqueName; | ||
} | ||
} | ||
// create the new array | ||
@@ -187,6 +214,6 @@ if (!_res[_uniqueMarkerName]) { | ||
name : _arrayName, | ||
type : 'array', | ||
type : (_isIteratorUsedInCurrentArray === true) ? 'array' : 'objectInArray', | ||
parent : _prevMarker, | ||
parents : _parents.slice(0), | ||
position : {start : _markerPos}, | ||
position : {}, | ||
iterators : [], | ||
@@ -196,2 +223,9 @@ xmlParts : [] | ||
} | ||
if (_isIteratorUsedInCurrentArray === true && _res[_uniqueMarkerName].position.start === undefined) { | ||
_res[_uniqueMarkerName].position.start = _markerPos; | ||
} | ||
if (_isIteratorUsedInCurrentArray === false) { | ||
_res[_uniqueMarkerName].conditions = _conditionsToFindObjectInArray; | ||
_conditionsToFindObjectInArray = []; | ||
} | ||
_prevMarker = _uniqueMarkerName; | ||
@@ -207,5 +241,5 @@ } | ||
var _iteratorStr = _iterator.str; | ||
// if the iterator is in a sub-object. Ex: movie.sort+1 | ||
if (_prevIteratorStr !== _iteratorStr) { | ||
var _iteratorInfo = {}; | ||
// if the iterator is in a sub-object. Ex: movie.sort+1 | ||
if (/\./.test(_iteratorStr)===true) { | ||
@@ -258,3 +292,3 @@ var _splitIterator = _iteratorStr.split('.'); | ||
}; | ||
if (_conditions.length>0) { | ||
if (_conditions.length > 0) { | ||
_xmlPart.conditions = _conditions; | ||
@@ -327,2 +361,3 @@ } | ||
// And sort them by ascendant positions | ||
@@ -367,2 +402,6 @@ extracter.sortXmlParts(_allSortedParts); | ||
var _partParents = descriptor[_part.obj].parents; | ||
// if the part belongs to the current visited array, keep it in this array. | ||
if (_part.pos <= _arrayPos.end && (_part.depth === _arrayDepth && _lastVisitedArrayName === _part.obj || _part.array === 'start' || _part.array === 'end' )) { | ||
break; | ||
} | ||
if (_part.pos > _arrayPos.start && _part.pos < _arrayPos.end && _partParents.indexOf(_lastVisitedArrayName) === -1 && _lastVisitedArrayName !== _part.obj) { | ||
@@ -565,3 +604,3 @@ _part.moveTo = _lastVisitedArrayName; | ||
parents : [], // TODO use _obj.parents | ||
hasOnlyObjects : (_obj.type === 'object') ? true : false, | ||
hasOnlyObjects : (_obj.type !== 'array') ? true : false, | ||
branchName : '' | ||
@@ -590,3 +629,3 @@ }; | ||
var _objParent = descriptor.dynamicData[_objParentName]; | ||
if (_objParent.type !== 'object') { | ||
if (_objParent.type === 'array') { | ||
_obj.hasOnlyObjects = false; | ||
@@ -634,6 +673,6 @@ } | ||
// if the depth is the same, put objects before arrays | ||
if (a.type === 'object' && b.type === 'array') { | ||
if (a.type !== 'array' && b.type === 'array') { | ||
return -1; | ||
} | ||
else if (a.type === 'array' && b.type === 'object') { | ||
else if (a.type === 'array' && b.type !== 'array') { | ||
return 1; | ||
@@ -640,0 +679,0 @@ } |
@@ -67,2 +67,6 @@ var fs = require('fs'); | ||
cleanJavascriptVariable : function(attributeName) { | ||
return attributeName.replace(/[^a-zA-Z0-9$_]/g, '_'); | ||
}, | ||
/** | ||
@@ -69,0 +73,0 @@ * Remove a directory and all its content |
@@ -45,3 +45,5 @@ var fs = require('fs'); | ||
} | ||
translator.loadTranslations(params.templatePath); | ||
if (options.translations === undefined) { | ||
translator.loadTranslations(params.templatePath); | ||
} | ||
} | ||
@@ -154,2 +156,3 @@ if (options.tempPath !== undefined && fs.existsSync(params.tempPath) === false) { | ||
// open the template (unzip if necessary) | ||
options.extension = path.extname(templatePath).toLowerCase().slice(1); | ||
file.openTemplate(templatePath, function (err, template) { | ||
@@ -159,3 +162,3 @@ if (err) { | ||
} | ||
if (path.extname(templatePath).toLowerCase().slice(1) === options.convertTo) { | ||
if (options.extension === options.convertTo) { | ||
options.convertTo = null; // avoid calling LibreOffice if the output file type is the same as the input file type | ||
@@ -238,2 +241,3 @@ } | ||
convertTo : options.convertTo, | ||
extension : options.extension, | ||
formatters : carbone.formatters, | ||
@@ -240,0 +244,0 @@ existingVariables : variables |
@@ -20,3 +20,3 @@ var helper = require('./helper'); | ||
// "?:" avoiding capturing | ||
var _cleanedXml = xml.replace(/(\r\n|\n|\r)/g,' ').replace(/\{([\s\S]+?)\}/g, function (m, text, offset) { | ||
var _cleanedXml = xml.replace(/(\r\n|\n|\r)/g,' ').replace(/\{([^{]+?)\}/g, function (m, text, offset) { | ||
// clean the marker | ||
@@ -27,3 +27,3 @@ var _marker = that.extractMarker(text); | ||
var _cleanedMarker = that.removeWhitespace(that.cleanMarker(_marker)); | ||
if (/^(?:d\.|d\[|c\.|c\[|\$)/.test(_cleanedMarker) === false) { | ||
if (/^(?:d[\.\[:]|c[\.\[:]|\$)/.test(_cleanedMarker) === false) { | ||
return m; | ||
@@ -287,2 +287,3 @@ } | ||
} | ||
parser.assignLoopId(markers); | ||
callback(null, markers); | ||
@@ -292,2 +293,39 @@ }, | ||
/** | ||
* 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 | ||
if (match = _loopWithRowNumberRegex.exec(_marker.name)) { | ||
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 | ||
@@ -325,3 +363,3 @@ * | ||
if (_openingTagIndex[_tagCount] === undefined) { | ||
return -1; | ||
return 0; | ||
} | ||
@@ -332,3 +370,3 @@ else if (_openingTagIndex[_tagCount] < indexWhereToStopSearch) { | ||
else { | ||
return (_lastOpeningTagIndexBeforeStop[_tagCount]!==undefined)? _lastOpeningTagIndexBeforeStop[_tagCount] : -1 ; | ||
return (_lastOpeningTagIndexBeforeStop[_tagCount]!==undefined)? _lastOpeningTagIndexBeforeStop[_tagCount] : 0 ; | ||
} | ||
@@ -393,5 +431,13 @@ }, | ||
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) { | ||
@@ -411,2 +457,5 @@ var _tagType = ''; | ||
} | ||
if (_tagCount > 0) { | ||
_hasAChanceToContainAtLeastOnePivot = true; | ||
} | ||
if (_tagCount > _highestTagCount) { | ||
@@ -432,3 +481,3 @@ _highestTagCount = _tagCount; | ||
} | ||
if (_highestTags.length===0 || (_highestTags.length===1 && _highestTags[0].type!=='x') || _highestTags[0].type==='<') { | ||
if ( _tagCount !== 0 && (_highestTags.length===0 || (_highestTags.length===1 && _highestTags[0].type!=='x') || _highestTags[0].type==='<')) { | ||
return null; | ||
@@ -452,2 +501,10 @@ } | ||
} | ||
// 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; | ||
@@ -454,0 +511,0 @@ }, |
@@ -33,3 +33,3 @@ var path = require('path'); | ||
}, | ||
/** | ||
@@ -36,0 +36,0 @@ * [XLSX] Convert shared string to inline string in Excel format in order to be compatible with Carbone algorithm |
{ | ||
"name": "carbone", | ||
"description": "Fast, Simple and Powerful report generator. Injects JSON and produces PDF, DOCX, XLSX, ODT, PPTX, ODS, ...!", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"bin": "bin/carbone", | ||
@@ -24,2 +24,3 @@ "main": "./lib", | ||
"moxie-zip": "=0.0.3", | ||
"timsort": "=0.3.0", | ||
"yauzl": "=2.8.0" | ||
@@ -26,0 +27,0 @@ }, |
@@ -17,2 +17,3 @@ Carbone.io | ||
- [Minimum Requirements](#minimum-requirements) | ||
- [Optional](#optional) | ||
- [Getting started](#getting-started) | ||
@@ -42,3 +43,3 @@ - [Basic sample](#basic-sample) | ||
- **Unlimited design** : The limit is your document editor: pagination, headers, footers, tables... | ||
- **Convert documents** : thanks to the integrated document converter (LibreOffice must be installed) | ||
- **Convert documents** : thanks to the integrated document converter | ||
- **Unique template engine** : Insert JSON-like markers `{d.companyName}` directly in your document | ||
@@ -66,6 +67,9 @@ - **Flexible** : Use any XML documents as a template: docx, odt, ods, xlsx, html, pptx, odp, custom xml files... | ||
- NodeJS 4.x+ | ||
- LibreOffice, if you want to use the document converter and generate PDF | ||
- Runs on OSX, Linux (servers and desktop), and coming soon on Windows | ||
#### Optional | ||
- LibreOffice server if you want to use the document converter and generate PDF. Without LibreOffice, you can still generate docx, xlsx, pptx, odt, ods, odp, html as long as your template is in the same format. | ||
## Getting started | ||
@@ -442,2 +446,6 @@ | ||
- Fabien Bigant | ||
- Maxime Magne | ||
- Vincent Bertin | ||
- Léo Labruyère | ||
- Aurélien Kermabon | ||
@@ -444,0 +452,0 @@ Thanks to all French citizens (Crédit Impôt Recherche, Jeune Entreprise Innovante, BPI)! |
290745
4228
452
5
29
+ Addedtimsort@=0.3.0