UNPKG

officegen-dbb

Version:

Modification of Ziv Barber's officegen, to provide a reasonable way to have line breaks in table cells.

693 lines (591 loc) 24.1 kB
// // 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. * <br /><br /> * List of data members: * <ul> * <li>features (object) - ???.</li> * <ul> * <li>type</li> * <li>outputType (string) - The type of the container to hold all the resources.</li> * </ul> * <li>pages</li> * <li>resources</li> * <li>type</li> * <li>plugs</li> * <li>length</li> * </ul> * @namespace officegen#private */ var privateData = {}; // Features been configured by the type selector and you can't change them: privateData.features = {}; privateData.features.type = {}; privateData.features.outputType = 'zip'; // privateData.features.page_name // Resources and "pages" of the document: privateData.pages = []; // Information about all the pages to create. privateData.resources = []; // List of all the resources to create inside the zip. // Extra data needed by the document and it's specific to either to "document prototype" (like MS-office) or a document type (like pptx): 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 } ); 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 { // now we can remove all temporary resources privateData.resources.forEach(function (resource) { if (resource.removed_after_used){ var filename = resource.data || resource.name; if ( officegenGlobals.settings.verbose ) { console.log("[officegen] Removing resource: ", filename); } fs.unlinkSync(filename); } }); // 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; }, /** * Get if we need verbose mode. * @param {string} docType Optional, Allow filtering by document type. * @param {string} moduleName Optional, Allow filtering by feature / module. * @memberof officegen#plugins */ getVerboseMode: function ( docType, moduleName ) { if ( !docType && !moduleName ) { return officegenGlobals.settings.verbose ? true : false; } // Endif. var verboseFlag; if ( officegenGlobals.settings.verbose && typeof officegenGlobals.settings.verbose === 'object' ) { if ( docType && officegenGlobals.settings.verbose.docType && typeof officegenGlobals.settings.verbose.docType === 'object' && officegenGlobals.settings.verbose.docType.indexOf ) { verboseFlag = (officegenGlobals.settings.verbose.docType.indexOf ( docType ) >= 0) ? true : false; } // Endif. if ( verboseFlag !== false && moduleName && officegenGlobals.settings.verbose.moduleName && typeof officegenGlobals.settings.verbose.moduleName === 'object' && officegenGlobals.settings.verbose.moduleName.indexOf ) { verboseFlag = (officegenGlobals.settings.verbose.moduleName.indexOf ( moduleName ) >= 0) ? true : false; } // Endif. } // Endif. return verboseFlag ? true : false; } }; module.exports.plugins = plugins; module.exports.schema = officegenGlobals.types; module.exports.docType = { "TEXT" : 1, "SPREADSHEET" : 2, "PRESENTATION" : 3 }; }) ();