//
// officegen: basic common code
//
// Please refer to README.md for this module's documentations.
//
// NOTE:
// - Before changing this code please refer to the hacking the code section on README.md.
//
// Copyright (c) 2013 Ziv Barber;
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// 'Software'), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
require("setimmediate"); // To be compatible with all versions of node.js
(function () {
var sys = require('util');
var events = require('events');
var Transform = require('stream').Transform || require('readable-stream/transform');
// Used by generate:
var archiver = require('archiver');
var fs = require('fs');
var PassThrough = require('stream').PassThrough || require('readable-stream/passthrough');
// Global data shared by all the officegen objects:
var officegenGlobals = {}; // Our internal global objects.
officegenGlobals.settings = {};
officegenGlobals.types = {};
officegenGlobals.docPrototypes = {};
officegenGlobals.resParserTypes = {};
/**
* The constructor of the office generator object.
* <br /><br />
* This constructor function is been called by makegen().
*
* <h3><b>The options:</b></h3>
*
* The configuration options effecting the operation of the officegen object. Some of them can be only been
* declared on the 'options' object passed to the constructor object and the rest can be configured by either
* a property with the same name or by special function.
*
* <h3><b>List of options:</b></h3>
*
* <ul>
* <li>'type' - the type of generator to create. Possible options: either 'pptx', 'docx' or 'xlsx'.</li>
* <li>'creator' - the name of the document's author. The default is 'officegen'.</li>
* <li>'onend' - callback that been fired after finishing to create the zip stream.</li>
* <li>'onerr' - callback that been fired on error.</li>
* </ul>
*
* @param {object} options List of configuration options (see in the description of this function).
* @constructor
* @name officegen
*/
var officegen = function ( options ) {
if ( false === ( this instanceof officegen )) {
return new officegen ( options );
} // Endif.
events.EventEmitter.call ( this );
// Transform.call ( this, { objectMode : true } );
// Internal events for plugins - NOT for the user:
// event 'beforeGen'
// event 'afterGen'
// event 'clearDoc'
var genobj = this; // Can be accessed by all the functions been declared inside the officegen object.
/**
* For all the private data of each officegen instance that we don't want the user of officegen to access it.
* Each officegen object has it's own copy of the private object so changes been done to the private object of one officegen document will not effect other objects.
* @namespace officegen#private
*/
var privateData = {};
privateData.features = {}; // Features been configured by the type selector and you can't change them.
privateData.features.type = {};
privateData.features.outputType = 'zip';
// privateData.features.page_name
privateData.pages = []; // Information about all the pages to create.
privateData.resources = []; // List of all the resources to create inside the zip.
privateData.type = {};
/**
* Combine the given options and the default values.
* <br /><br />
*
* This function creating the real options object.
*
* @param {object} options The options to configure.
*/
function setOptions ( object, source ) {
object = object || {};
var objectTypes = {
'boolean': false,
'function': true,
'object': true,
'number': false,
'string': false,
'undefined': false
};
function isObject (value) {
return !!(value && objectTypes[typeof value]);
}
function keys (object) {
if (!isObject(object)) {
return [];
}
return Object.keys(object);
}
var index;
var iterable = object;
var result = iterable;
var args = arguments;
var argsIndex = 0;
var argsLength = args.length;
//loop variables
var ownIndex = -1;
var ownProps = objectTypes[typeof iterable] && keys(iterable);
var length = ownProps ? ownProps.length : 0;
while (++argsIndex < argsLength) {
iterable = args[argsIndex];
if (iterable && objectTypes[typeof iterable]) {
while (++ownIndex < length) {
index = ownProps[ownIndex];
if (typeof result[index] === 'undefined' || result[index] === null) {
result[index] = iterable[index];
} else if (isObject(result[index]) && isObject(iterable[index])) {
result[index] = setOptions(result[index], iterable[index]);
} // Endif.
} // End of while loop.
} // Endif.
} // End of while loop.
return result;
}
/**
* Configure this object to generate the given type of document.
* <br /><br />
*
* Called by the document constructor to configure the new document object to the given type.
*
* @param {string} new_type The type of document to create.
*/
function setGeneratorType ( new_type ) {
privateData.length = 0;
var is_ok = false;
if ( new_type ) {
for ( var cur_type in officegenGlobals.types ) {
if ( (cur_type == new_type) && officegenGlobals.types[cur_type] && officegenGlobals.types[cur_type].createFunc ) {
officegenGlobals.types[cur_type].createFunc ( genobj, new_type, genobj.options, privateData, officegenGlobals.types[cur_type] );
is_ok = true;
break;
} // Endif.
} // End of for loop.
if ( !is_ok ) {
// console.error ( '\nFATAL ERROR: Either unknown or unsupported file type - %s\n', options.type );
genobj.emit ( 'error', 'FATAL ERROR: Invalid file type.' );
} // Endif.
} // Endif.
}
/**
* API for plugins.
* <br /><br />
* Officegen plugins can extend officegen to support more document formats.
* <br /><br />
* Examples how to do it can be found on lib/gendocx.js, lib/genpptx.js and lib/genxlsx.js.
* @namespace officegen#private#plugs
* @example <caption>Adding a new document type to officegen</caption>
* var baseobj = require ( "officegen" );
*
* function makeMyDoc ( officegenObj, typeCodeName, options, officegenObjPlugins, typeInfo ) {
* // officegenObjPlugins = Plugins access to extend officegenObj.
* }
*
* baseobj.plugins.registerDocType (
* 'mydoctype', // The document type's code name.
* makeMyDoc,
* {},
* baseobj.docType.TEXT,
* "My Special Document File Format"
* );
*/
privateData.plugs = {
/**
* Add a resource to the list of resources to place inside the output zip file.
* <br /><br />
*
* This method adding a resource to the list of resources to place inside the output document ZIP.
* <br />
* Changed by vtloc in 2014Jan10.
*
* @param {string} resource_name The name of the resource (path).
* @param {string} type_of_res The type of this resource: either 'file', 'buffer', 'stream' or 'officegen' (the last one allow you to put office document inside office document).
* @param {object} res_data Optional data to use when creating this resource.
* @param {function} res_cb Callback to generate this resource (for 'buffer' mode only).
* @param {boolean} is_always Is true if this resource is perment for all the zip of this document type.
* @param {boolean} removed_after_used Is true if we need to delete this file after used.
* @memberof officegen#private#plugs
*/
intAddAnyResourceToParse: function ( resource_name, type_of_res, res_data, res_cb, is_always, removed_after_used ) {
var newRes = {};
newRes.name = resource_name;
newRes.type = type_of_res;
newRes.data = res_data;
newRes.callback = res_cb;
newRes.is_perment = is_always;
// delete the temporatory resources after used
// @author vtloc
// @date 2014Jan10
if ( removed_after_used ) {
newRes.removed_after_used = removed_after_used;
} else {
newRes.removed_after_used = false;
} // Endif.
if ( officegenGlobals.settings.verbose ) {
console.log("[officegen] Push new res : ", newRes);
} // Endif.
privateData.resources.push ( newRes );
},
/**
* Any additional plugin API must be placed here.
* @memberof officegen#private#plugs
*/
type: {}
};
// Public API:
/**
* Generating the output document stream.
* <br /><br />
*
* The user of officegen must call this method after filling all the information about what to put inside
* the generated document. This method is creating the output document directly into the given stream object.
*
* The options parameters properties:
*
* 'finalize' - callback to be called after finishing to generate the document.
* 'error' - callback to be called on error.
*
* @param {object} output_stream The stream to receive the generated document.
* @param {object} options Way to pass callbacks.
* @function generate
* @memberof officegen
* @instance
*/
this.generate = function ( output_stream, options ) {
if ( officegenGlobals.settings.verbose ) {
console.log("[officegen] Start generate() : ", {outputType: privateData.features.outputType });
}
if ( typeof options == 'object' ) {
if ( options.finalize ) {
genobj.on ( 'finalize', options.finalize );
} // Endif.
if ( options.error ) {
genobj.on ( 'error', options.error );
} // Endif.
} // Endif.
if ( privateData.features.page_name ) {
if ( privateData.pages.length == 0 ) {
genobj.emit ( 'error', 'ERROR: No ' + privateData.features.page_name + ' been found inside your document.' );
} // Endif.
} // Endif.
// Allow the type generator to prepare everything:
genobj.emit ( 'beforeGen', privateData );
var archive = archiver( privateData.features.outputType == 'zip' ? 'zip' : 'tar' );
/**
* Error handler.
* <br /><br />
*
* This is our error handler method for creating archive.
*
* @param {string} err The error string.
*/
function onArchiveError ( err ) {
genobj.emit ( 'error', err );
}
archive.on ( 'error', onArchiveError );
if ( privateData.features.outputType == 'gzip' ) {
var zlib = require('zlib');
var gzipper = zlib.createGzip ();
archive.pipe ( gzipper ).pipe ( output_stream );
} else {
archive.pipe ( output_stream );
} // Endif.
/**
* Add the next resource into the zip stream.
* <br /><br />
*
* This function adding the next resource into the zip stream.
*/
function generateNextResource ( cur_index )
{
if ( officegenGlobals.settings.verbose ) {
console.log("[officegen] generateNextResource("+cur_index+") : ", privateData.resources[cur_index]);
}
var resStream;
if ( cur_index < privateData.resources.length ) {
if ( typeof privateData.resources[cur_index] != 'undefined' ) {
switch ( privateData.resources[cur_index].type ) {
// Generate the resource text data by calling to provided function:
case 'buffer':
resStream = privateData.resources[cur_index].callback ( privateData.resources[cur_index].data );
break;
// Just copy the file as is:
case 'file':
resStream = fs.createReadStream ( privateData.resources[cur_index].data || privateData.resources[cur_index].name );
break;
// Just use this stream:
case 'stream':
resStream = privateData.resources[cur_index].data;
break;
// Officegen object:
case 'officegen':
resStream = new PassThrough ();
privateData.resources[cur_index].data.generate ( resStream );
break;
// Custom parser:
default:
for ( var cur_parserType in officegenGlobals.resParserTypes ) {
if ( (cur_parserType == privateData.resources[cur_index].type) && officegenGlobals.resParserTypes[cur_parserType] && officegenGlobals.resParserTypes[cur_parserType].parserFunc ) {
resStream = officegenGlobals.resParserTypes[cur_parserType].parserFunc (
genobj,
privateData.resources[cur_index].name,
privateData.resources[cur_index].callback, // Can be used as the template source for template engines.
privateData.resources[cur_index].data, // The data for the template engine.
officegenGlobals.resParserTypes[cur_parserType].extra_data
);
break;
} // Endif.
} // End of for loop.
} // End of switch.
if ( typeof resStream != 'undefined' ) {
if ( officegenGlobals.settings.verbose ) {
console.log ( '[officegen] Adding into archive : "' + privateData.resources[cur_index].name + '" (' + privateData.resources[cur_index].type + ')...' );
} // Endif.
archive.append ( resStream, { name: privateData.resources[cur_index].name } );
if( privateData.resources[cur_index].removed_after_used )
{
// delete the temporatory resources after used
// @author vtloc
// @date 2014Jan10
var fileName = privateData.resources[cur_index].data || privateData.resources[cur_index].name;
fs.unlinkSync( fileName );
} // Endif.
generateNextResource ( cur_index + 1 );
} else {
if ( officegenGlobals.settings.verbose ) {
console.log("[officegen] resStream is undefined"); // is it normal ??
}
generateNextResource ( cur_index + 1 );
// setImmediate ( function() { generateNextResource ( cur_index + 1 ); });
} // Endif.
} else {
// Removed resource - just ignore it:
generateNextResource ( cur_index + 1 );
// setImmediate ( function() { generateNextResource ( cur_index + 1 ); });
} // Endif.
} else {
// No more resources to add - close the archive:
if ( officegenGlobals.settings.verbose ) {
console.log("[officegen] Finalizing archive ...");
}
archive.finalize ();
// Event to the type generator:
genobj.emit ( 'afterGen', privateData, null, archive.pointer () );
genobj.emit ( 'finalize', archive.pointer () );
} // Endif.
}
// Start the process of generating the output zip stream:
generateNextResource ( 0 );
};
/**
* Reuse this object for a new document of the same type.
* <br /><br />
*
* Call this method if you want to start generating a new document of the same type using this object.
* @function startNewDoc
* @memberof officegen
* @instance
*/
this.startNewDoc = function () {
var kill = [];
for ( var i = 0; i < privateData.resources.length; i++ ) {
if ( !privateData.resources[i].is_perment ) kill.push ( i );
} // End of for loop.
for ( var i = 0; i < kill.length; i++ ) privateData.resources.splice ( kill[i] - i, 1 );
privateData.pages.length = 0;
genobj.emit ( 'clearDoc', privateData );
};
// Public API - plugin API:
/**
* Register a new resource to add into the generated ZIP stream.
* <br /><br />
*
* Using this method the user can add extra custom resources into the generated ZIP stream.
*
* @param {string} resource_name The name of the resource (path).
* @param {string} type_of_res The type of this resource: either 'file' or 'buffer'.
* @param {object} res_data Optional data to use when creating this resource.
* @param {function} res_cb Callback to generate this resource (for 'buffer' mode only).
* @function addResourceToParse
* @memberof officegen
* @instance
*/
this.addResourceToParse = function ( resource_name, type_of_res, res_data, res_cb ) {
// We don't want the user to add permanent resources to the list of resources:
privateData.plugs.intAddAnyResourceToParse ( resource_name, type_of_res, res_data, res_cb, false );
};
if ( typeof options == 'string' ) {
options = { 'type': options };
} // Endif.
// See the officegen descriptions for the rules of the options:
genobj.options = setOptions ( options, { 'type': 'unknown' } );
if ( genobj.options && genobj.options.onerr ) {
genobj.on ( 'error', genobj.options.onerr );
} // Endif.
if ( genobj.options && genobj.options.onend ) {
genobj.on ( 'finalize', genobj.options.onend );
} // Endif.
// Configure this object depending on the user's selected type:
if ( genobj.options.type ) {
setGeneratorType ( genobj.options.type );
} // Endif.
return this;
};
sys.inherits ( officegen, events.EventEmitter );
/**
* Create a new officegen object.
* <br /><br />
*
* This method creating a new officegen based object.
*/
module.exports = function ( options ) {
return new officegen ( options );
};
/**
* Change the verbose state of officegen.
* <br /><br />
*
* This is a global settings effecting all the officegen objects in your application. You should
* use it only for debugging.
*
* @param {boolean} new_state Either true or false.
*/
module.exports.setVerboseMode = function setVerboseMode ( new_state ) {
officegenGlobals.settings.verbose = new_state;
}
/**
* Plugin API effecting all the instances of the officegen object.
*
* @namespace officegen#plugins
*/
var plugins = {
/**
* Register a new type of document that we can generate.
* <br /><br />
*
* This method registering a new type of document that we can generate. You can extend officegen to support any
* type of document that based on resources files inside ZIP stream.
*
* @param {string} typeName The type of the document file.
* @param {function} createFunc The function to use to create this type of file.
* @param {object} schema_data Information needed by Schema-API to generate this kind of document.
* @param {string} docType Document type.
* @param {string} displayName The display name of this type.
* @memberof officegen#plugins
*/
registerDocType: function ( typeName, createFunc, schema_data, docType, displayName ) {
officegenGlobals.types[typeName] = {};
officegenGlobals.types[typeName].createFunc = createFunc;
officegenGlobals.types[typeName].schema_data = schema_data;
officegenGlobals.types[typeName].type = docType;
officegenGlobals.types[typeName].display = displayName;
},
/**
* Get a document type object by name.
* <br /><br />
*
* This method get a document type object.
*
* @param {string} typeName The name of the document type.
* @return The plugin object of the document type.
* @memberof officegen#plugins
*/
getDocTypeByName: function ( typeName ) {
return officegenGlobals.types[typeName];
},
/**
* Register a document prototype object.
* <br /><br />
*
* This method registering a prototype document object. You can place all the common code needed by a group of document
* types in a single prototype object.
*
* @param {string} typeName The name of the prototype object.
* @param {object} baseObj The prototype object.
* @param {string} displayName The display name of this type.
* @memberof officegen#plugins
*/
registerPrototype: function ( typeName, baseObj, displayName ) {
officegenGlobals.docPrototypes[typeName] = {};
officegenGlobals.docPrototypes[typeName].baseObj = baseObj;
officegenGlobals.docPrototypes[typeName].display = displayName;
},
/**
* Get a document prototype object by name.
* <br /><br />
*
* This method get a prototype object.
*
* @param {string} typeName The name of the prototype object.
* @return The prototype plugin object.
* @memberof officegen#plugins
*/
getPrototypeByName: function getPrototypeByName ( typeName ) {
return officegenGlobals.docPrototypes[typeName];
},
/**
* Register a new resource parser.
* <br /><br />
*
* This method registering a new resource parser. One use of this feature is in case that you are developing a new
* type of document and you want to extend officegen to use some kind of template engine as jade, ejs, haml* or CoffeeKup.
* In this case you can use a template engine to generate one or more of the resources inside the output archive.
* Another use of this method is to replace an existing plugin with different implementation.
*
* @param {string} typeName The type of the parser plugin.
* @param {function} parserFunc The resource generating function.
* @param {object} extra_data Optional additional data that may be required by the parser function.
* @param {string} displayName The display name of this type.
* @memberof officegen#plugins
*/
registerParserType: function ( typeName, parserFunc, extra_data, displayName ) {
officegenGlobals.resParserTypes[typeName] = {};
officegenGlobals.resParserTypes[typeName].parserFunc = parserFunc;
officegenGlobals.resParserTypes[typeName].extra_data = extra_data;
officegenGlobals.resParserTypes[typeName].display = displayName;
}
};
module.exports.plugins = plugins;
module.exports.schema = officegenGlobals.types;
module.exports.docType = { "TEXT" : 1, "SPREADSHEET" : 2, "PRESENTATION" : 3 };
}) ();