Source: lib/basicgen.js

//
// 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 };

}) ();